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

ElasticSearchAdapter   A

Complexity

Total Complexity 9

Size/Duplication

Total Lines 123
Duplicated Lines 0 %

Importance

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