Passed
Pull Request — main (#2)
by Dante
01:33
created

ElasticSearchAdapter   A

Complexity

Total Complexity 9

Size/Duplication

Total Lines 121
Duplicated Lines 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
wmc 9
eloc 51
c 2
b 1
f 0
dl 0
loc 121
rs 10

5 Methods

Rating   Name   Duplication   Size   Complexity  
A buildQuery() 0 29 3
A buildElasticSearchQuery() 0 3 1
A createTempTable() 0 47 3
A search() 0 3 1
A indexResource() 0 2 1
1
<?php
2
declare(strict_types=1);
3
4
namespace BEdita\ElasticSearch\Adapter;
5
6
use BEdita\Core\Search\BaseAdapter;
7
use Cake\Database\Connection;
8
use Cake\Database\Expression\ComparisonExpression;
9
use Cake\Database\Expression\IdentifierExpression;
10
use Cake\Database\Schema\TableSchema;
11
use Cake\Datasource\EntityInterface;
12
use Cake\Log\LogTrait;
13
use Cake\ORM\Locator\LocatorAwareTrait;
14
use Cake\ORM\Query;
15
use Cake\ORM\Table;
16
use Exception;
17
use Psr\Log\LogLevel;
18
19
class ElasticSearchAdapter extends BaseAdapter
20
{
21
    use LocatorAwareTrait;
22
    use LogTrait;
23
24
    /**
25
     * @inheritDoc
26
     */
27
    public function search(Query $query, string $text, array $options = [], array $config = []): Query
28
    {
29
        return $this->buildQuery($query, $options);
30
    }
31
32
    /**
33
     * {@inheritDoc}
34
     *
35
     * @codeCoverageIgnore
36
     */
37
    public function indexResource(EntityInterface $entity, string $operation): void
38
    {
39
    }
40
41
    /**
42
     * Build elastic search query
43
     */
44
    protected function buildElasticSearchQuery(array $options): array
45
    {
46
        return [];
47
    }
48
49
    /**
50
     * Build query and return it
51
     *
52
     * @param \Cake\ORM\Query $query The query
53
     * @param array $options The options
54
     * @return \Cake\ORM\Query
55
     */
56
    protected function buildQuery(Query $query, array $options): Query
57
    {
58
        $results = $this->buildElasticSearchQuery($options);
59
60
        if (count($results) === 0) {
61
            // Nothing found. No results should be returned. Add a contradiction to the `WHERE` clause.
62
            return $query->where(new ComparisonExpression('1', '1', 'integer', '<>'));
63
        }
64
65
        // Prepare temporary table with `id` and `score` from ElasticSearch results.
66
        $tempTable = $this->createTempTable($query->getConnection());
67
        $insertQuery = $tempTable->query()->insert(['id', 'score']);
68
        foreach ($results as $row) {
69
            $insertQuery = $insertQuery->values($row);
70
        }
71
        $insertQuery->execute();
72
73
        // Add a join with the temporary table to filter by ID and sort by relevance score.
74
        return $query
75
            ->innerJoin(
76
                $tempTable->getTable(),
77
                new ComparisonExpression(
78
                    new IdentifierExpression($tempTable->aliasField('id')),
79
                    new IdentifierExpression($this->fetchTable()->aliasField('id')),
80
                    'integer',
81
                    '='
82
                )
83
            )
84
            ->orderDesc($tempTable->aliasField('score'));
85
    }
86
87
    /**
88
     * Create a temporary table to store search results.
89
     *
90
     * @param \Cake\Database\Connection $connection The database connection
91
     * @return \Cake\ORM\Table|null
92
     */
93
    protected function createTempTable(Connection $connection): ?Table
94
    {
95
        $table = sprintf('elasticsearch_%s', time());
96
        $schema = (new TableSchema($table))
97
            ->setTemporary(true)
98
            ->addColumn('id', [
99
                'type' => TableSchema::TYPE_INTEGER,
100
                'length' => 11,
101
                'unsigned' => true,
102
                'null' => false,
103
            ])
104
            ->addColumn('score', [
105
                'type' => TableSchema::TYPE_FLOAT,
106
                'null' => false,
107
            ])
108
            ->addConstraint(
109
                'PRIMARY',
110
                [
111
                    'type' => TableSchema::CONSTRAINT_PRIMARY,
112
                    'columns' => ['id'],
113
                ]
114
            )
115
            ->addIndex(
116
                sprintf('%s_score_idx', str_replace('_', '', $table)),
117
                [
118
                    'type' => TableSchema::INDEX_INDEX,
119
                    'columns' => ['score'],
120
                ]
121
            );
122
123
        try {
124
            // Execute SQL to create table. In MySQL the transaction is completely useless,
125
            // because `CREATE TABLE` implicitly implies a commit.
126
            $connection->transactional(function (Connection $connection) use ($schema): void {
127
                foreach ($schema->createSql($connection) as $statement) {
128
                    $connection->execute($statement);
129
                }
130
            });
131
        } catch (Exception $e) {
132
            $this->log($e->getMessage(), LogLevel::CRITICAL);
133
134
            return null;
135
        }
136
137
        return (new Table(compact('connection', 'table', 'schema')))
138
            ->setPrimaryKey('id')
139
            ->setDisplayField('score');
140
    }
141
}
142