Passed
Pull Request — main (#2)
by Dante
01:49 queued 47s
created

ElasticSearchAdapter   A

Complexity

Total Complexity 9

Size/Duplication

Total Lines 123
Duplicated Lines 0 %

Importance

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

5 Methods

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