Add product entity extension to elasticsearch

Overview

In this guide you'll learn how to add extended fields of the product entity to the elasticsearch engine to make it searchable.
In this example we'll assume an extension of the ProductDefinition with a string field customString like described in Adding Complex data to existing entities.

Prerequisites

This guide is built upon the Plugin Base Guide, and the entity extension described in Adding Complex data to existing entities. We will extend the product extension with an OneToOneAssociationField and OneToManyAssociationField.

Decorate the ElasticsearchProductDefinition

To extend the elasticsearch definition we need to extend the product definition first and add the subscriber. This is described in the above mentioned articles. Here we show you how this could look like in the end.
The service.xml with all needed definitions.
<plugin root>/src/Core/Content/DependencyInjection/product.xml
1
<?xml version="1.0" ?>
2
3
<container xmlns="http://symfony.com/schema/dic/services"
4
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
6
7
<services>
8
<service id="Swag\BasicExample\Extension\Content\Product\CustomExtension">
9
<tag name="shopware.entity.extension"/>
10
</service>
11
12
<service id="Swag\BasicExample\Extension\Content\Product\OneToOneExampleExtensionDefinition">
13
<tag name="shopware.entity.definition" entity="one_to_one_swag_example_extension" />
14
</service>
15
16
<service id="Swag\BasicExample\Extension\Content\Product\OneToManyExampleExtensionDefinition">
17
<tag name="shopware.entity.definition" entity="one_to_many_swag_example_extension" />
18
</service>
19
20
<service id="Swag\BasicExample\Subscriber\ProductSubscriber">
21
<tag name="kernel.event_subscriber"/>
22
</service>
23
24
<service id="Swag\BasicExample\Elasticsearch\Product\MyProductEsDecorator" decorates="Shopware\Elasticsearch\Product\ElasticsearchProductDefinition">
25
<argument type="service" id="Swag\BasicExample\Elasticsearch\Product\MyProductEsDecorator.inner"/>
26
<argument type="service" id="Doctrine\DBAL\Connection"/>
27
</service>
28
</services>
29
</container>
Copied!
The product extension CustomExtension.php provides the extensions to the product entity.
<plugin root>/src/Extension/Content/Product/CustomExtension.php
1
<?php declare(strict_types=1);
2
3
namespace Swag\BasicExample\Extension\Content\Product;
4
5
use Shopware\Core\Content\Product\ProductDefinition;
6
use Shopware\Core\Framework\DataAbstractionLayer\EntityExtension;
7
use Shopware\Core\Framework\DataAbstractionLayer\Field\ObjectField;
8
use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToManyAssociationField;
9
use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToOneAssociationField;
10
use Shopware\Core\Framework\DataAbstractionLayer\FieldCollection;
11
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Runtime;
12
13
class CustomExtension extends EntityExtension
14
{
15
public function extendFields(FieldCollection $collection): void
16
{
17
//Add ApiAware flag to make this field searchable
18
$collection->add(
19
(new OneToOneAssociationField('oneToOneExampleExtension', 'id', 'product_id', OneToOneExampleExtensionDefinition::class, true))->addFlags(new ApiAware())
20
);
21
//Add ApiAware flag to make this field searchable
22
$collection->add(
23
(new OneToManyAssociationField('oneToManyExampleExtension', OneToManyExampleExtensionDefinition::class, 'product_id'))->addFlags(new ApiAware())
24
);
25
//Runtime fields are not searchable
26
$collection->add(
27
(new ObjectField('custom_string', 'customString'))->addFlags(new Runtime())
28
);
29
}
30
31
public function getDefinitionClass(): string
32
{
33
return ProductDefinition::class;
34
}
35
}
Copied!
The entity definition OneToManyExampleExtensionDefinition.php.
<plugin root>/src/Extension/Content/Product/OneToManyExampleExtensionDefinition.php
1
<?php declare(strict_types=1);
2
3
namespace Swag\BasicExample\Extension\Content\Product;
4
5
use Shopware\Core\Content\Product\ProductDefinition;
6
use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
7
use Shopware\Core\Framework\DataAbstractionLayer\Field\FkField;
8
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\ApiAware;
9
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\PrimaryKey;
10
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Required;
11
use Shopware\Core\Framework\DataAbstractionLayer\Field\IdField;
12
use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToOneAssociationField;
13
use Shopware\Core\Framework\DataAbstractionLayer\Field\ReferenceVersionField;
14
use Shopware\Core\Framework\DataAbstractionLayer\Field\StringField;
15
use Shopware\Core\Framework\DataAbstractionLayer\FieldCollection;
16
use Shopware\Core\Framework\DataAbstractionLayer\Entity;
17
18
class OneToManyExampleExtensionDefinition extends EntityDefinition
19
{
20
public const ENTITY_NAME = 'one_to_many_swag_example_extension';
21
22
public function getEntityName(): string
23
{
24
return self::ENTITY_NAME;
25
}
26
27
public function getEntityClass(): string
28
{
29
return Entity::class;
30
}
31
32
protected function defineFields(): FieldCollection
33
{
34
return new FieldCollection([
35
(new IdField('id', 'id'))->addFlags(new ApiAware(), new Required(), new PrimaryKey()),
36
new FkField('product_id', 'productId', ProductDefinition::class),
37
(new ReferenceVersionField(ProductDefinition::class))->addFlags(new Required()),
38
(new StringField('custom_string', 'customString'))->addFlags(new ApiAware()),
39
40
new ManyToOneAssociationField('product', 'product_id', ProductDefinition::class),
41
]);
42
}
43
}
Copied!
The entity definition OneToOneExampleExtensionDefinition.php.
<plugin root>/src/Extension/Content/Product/OneToOneExampleExtensionDefinition.php
1
<?php declare(strict_types=1);
2
3
namespace Swag\BasicExample\Extension\Content\Product;
4
5
use Shopware\Core\Content\Product\ProductDefinition;
6
use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
7
use Shopware\Core\Framework\DataAbstractionLayer\Field\FkField;
8
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\ApiAware;
9
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\PrimaryKey;
10
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Required;
11
use Shopware\Core\Framework\DataAbstractionLayer\Field\IdField;
12
use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToOneAssociationField;
13
use Shopware\Core\Framework\DataAbstractionLayer\Field\ReferenceVersionField;
14
use Shopware\Core\Framework\DataAbstractionLayer\Field\StringField;
15
use Shopware\Core\Framework\DataAbstractionLayer\FieldCollection;
16
use Shopware\Core\Framework\DataAbstractionLayer\Entity;
17
18
class OneToOneExampleExtensionDefinition extends EntityDefinition
19
{
20
public const ENTITY_NAME = 'one_to_one_swag_example_extension';
21
22
public function getEntityName(): string
23
{
24
return self::ENTITY_NAME;
25
}
26
27
public function getEntityClass(): string
28
{
29
return Entity::class;
30
}
31
32
protected function defineFields(): FieldCollection
33
{
34
return new FieldCollection([
35
(new IdField('id', 'id'))->addFlags(new ApiAware(), new Required(), new PrimaryKey()),
36
new FkField('product_id', 'productId', ProductDefinition::class),
37
(new ReferenceVersionField(ProductDefinition::class))->addFlags(new Required()),
38
(new StringField('custom_string', 'customString'))->addFlags(new ApiAware()),
39
40
new OneToOneAssociationField('product', 'product_id', 'id', ProductDefinition::class, false)
41
]);
42
}
43
}
Copied!
Here is a decoration to add a new field named customString, an oneToOneAssociationField named oneToOneExampleExtension and an oneToManyAssociationField named oneToManyExampleExtension to the index. For adding more information from the database you should execute a single query with all document ids (array_column($documents, 'id')) and map the values.
<plugin root>/src/Elasticsearch/Product/MyProductEsDecorator.php
1
<?php
2
3
namespace Swag\BasicExample\Elasticsearch\Product;
4
5
use Shopware\Core\Framework\Context;
6
use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
7
use Shopware\Elasticsearch\Framework\AbstractElasticsearchDefinition;
8
use Shopware\Elasticsearch\Framework\Indexing\EntityMapper;
9
use Doctrine\DBAL\Connection;
10
use Swag\BasicExample\Subscriber\ProductSubscriber;
11
12
class MyProductEsDecorator extends AbstractElasticsearchDefinition
13
{
14
private AbstractElasticsearchDefinition $productDefinition;
15
private Connection $connection;
16
17
public function __construct(AbstractElasticsearchDefinition $productDefinition, Connection $connection)
18
{
19
$this->productDefinition = $productDefinition;
20
$this->connection = $connection;
21
}
22
23
public function getEntityDefinition(): EntityDefinition
24
{
25
return $this->productDefinition->getEntityDefinition();
26
}
27
28
/**
29
* Extend the mapping with your own changes
30
* Take care to get the default mapping first by `$this->productDefinition->getMapping($context);`
31
*/
32
public function getMapping(Context $context): array
33
{
34
$mapping = $this->productDefinition->getMapping($context);
35
36
//The mapping for a simple keyword field
37
$mapping['properties']['customString'] = EntityMapper::KEYWORD_FIELD;
38
39
// Adding an association as keyword
40
$mapping['properties']['oneToOneExampleExtension'] = [
41
'type' => 'nested',
42
'properties' => [
43
'customString' => EntityMapper::KEYWORD_FIELD,
44
],
45
];
46
47
// Adding a nested field with id
48
$mapping['properties']['oneToManyExampleExtension'] = [
49
'type' => 'nested',
50
'properties' => [
51
'id' => EntityMapper::KEYWORD_FIELD,
52
],
53
];
54
55
return $mapping;
56
}
57
58
public function fetch(array $ids, Context $context): array
59
{
60
$documents = $this->productDefinition->fetch($ids, $context);
61
62
$associationOneToOne = $this->fetchOneToOneExample($ids);
63
$associationOneToMany = $this->fetchOneToManyExample($ids);
64
65
foreach ($documents as &$document) {
66
/**
67
* A field directly on the product.
68
* The value should be filled with the same Runtime value which will be set by the ProductSubscriber
69
*/
70
$document['customString'] = ProductSubscriber::getRuntimeValue($document['id'])->getValue();
71
72
73
/**
74
* Field with value from associated entity
75
*/
76
if (isset($associationOneToOne[$document['id']])) {
77
$document['oneToOneExampleExtension']['customString'] = $associationOneToOne[$document['id']];
78
}
79
80
/**
81
* Field with multiple id entrys from associated entity
82
*/
83
if (isset($associationOneToMany[$document['id']])) {
84
$document['oneToManyExampleExtension'] = array_map(function (string $id) {
85
return ['id' => $id];
86
}, array_filter(explode('|', $associationOneToMany[$document['id']] ?? '')));
87
}
88
}
89
90
return $documents;
91
}
92
93
/**
94
* Read the associated entries directly from the database
95
*/
96
private function fetchOneToOneExample(array $ids): array
97
{
98
$query = <<<SQL
99
SELECT LOWER(HEX(product_id)) as id, custom_string
100
FROM one_to_one_swag_example_extension
101
WHERE
102
product_id IN(:ids)
103
SQL;
104
105
106
return $this->connection->fetchAllKeyValue(
107
$query,
108
[
109
'ids' => $ids,
110
],
111
[
112
'ids' => Connection::PARAM_STR_ARRAY
113
]
114
);
115
}
116
117
/**
118
* Read the associated entries directly from the database
119
*/
120
private function fetchOneToManyExample(array $ids): array
121
{
122
$query = <<<SQL
123
SELECT LOWER(HEX(product_id)) as id, GROUP_CONCAT(id SEPARATOR "|")
124
FROM one_to_many_swag_example_extension
125
WHERE
126
product_id IN(:ids)
127
SQL;
128
129
130
return $this->connection->fetchAllKeyValue(
131
$query,
132
[
133
'ids' => $ids,
134
],
135
[
136
'ids' => Connection::PARAM_STR_ARRAY
137
]
138
);
139
}
140
}
Copied!