Passed
Pull Request — master (#208)
by
unknown
02:17
created

SolrReindexBase::clearRecords()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 12
nc 4
nop 4
dl 0
loc 23
rs 9.0856
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\FullTextSearch\Solr\Reindex\Handlers;
4
5
use Psr\Log\LoggerInterface;
6
use SilverStripe\Core\Environment;
7
use SilverStripe\FullTextSearch\Solr\Solr;
8
use SilverStripe\FullTextSearch\Solr\SolrIndex;
9
use SilverStripe\FullTextSearch\Search\Variants\SearchVariant;
10
use SilverStripe\FullTextSearch\Search\Queries\SearchQuery;
11
use SilverStripe\ORM\DataObject;
12
use SilverStripe\ORM\DataList;
13
use SilverStripe\ORM\DB;
14
15
/**
16
 * Base class for re-indexing of solr content
17
 */
18
abstract class SolrReindexBase implements SolrReindexHandler
19
{
20
    public function runReindex(LoggerInterface $logger, $batchSize, $taskName, $classes = null)
21
    {
22
        foreach (Solr::get_indexes() as $indexInstance) {
23
            $this->processIndex($logger, $indexInstance, $batchSize, $taskName, $classes);
24
        }
25
    }
26
27
    /**
28
     * Process index for a single SolrIndex instance
29
     *
30
     * @param LoggerInterface $logger
31
     * @param SolrIndex $indexInstance
32
     * @param int $batchSize
33
     * @param string $taskName
34
     * @param string $classes
35
     */
36
    protected function processIndex(
37
        LoggerInterface $logger,
38
        SolrIndex $indexInstance,
39
        $batchSize,
40
        $taskName,
41
        $classes = null
42
    ) {
43
        // Filter classes for this index
44
        $indexClasses = $this->getClassesForIndex($indexInstance, $classes);
45
46
        // Clear all records in this index which do not contain the given classes
47
        $logger->info("Clearing obsolete classes from ".$indexInstance->getIndexName());
48
        $indexInstance->clearObsoleteClasses($indexClasses);
49
50
        // Build queue for each class
51
        foreach ($indexClasses as $class => $options) {
52
            $includeSubclasses = $options['include_children'];
53
54
            foreach (SearchVariant::reindex_states($class, $includeSubclasses) as $state) {
55
                $this->processVariant($logger, $indexInstance, $state, $class, $includeSubclasses, $batchSize, $taskName);
56
            }
57
        }
58
    }
59
60
    /**
61
     * Get valid classes and options for an index with an optional filter
62
     *
63
     * @param SolrIndex $index
64
     * @param string|array $filterClasses Optional class or classes to limit to
65
     * @return array List of classes, where the key is the classname and value is list of options
66
     */
67
    protected function getClassesForIndex(SolrIndex $index, $filterClasses = null)
68
    {
69
        // Get base classes
70
        $classes = $index->getClasses();
71
        if (!$filterClasses) {
72
            return $classes;
73
        }
74
75
        // Apply filter
76
        if (!is_array($filterClasses)) {
77
            $filterClasses = explode(',', $filterClasses);
78
        }
79
        return array_intersect_key($classes, array_combine($filterClasses, $filterClasses));
80
    }
81
82
    /**
83
     * Process re-index for a given variant state and class
84
     *
85
     * @param LoggerInterface $logger
86
     * @param SolrIndex $indexInstance
87
     * @param array $state Variant state
88
     * @param string $class
89
     * @param bool $includeSubclasses
90
     * @param int $batchSize
91
     * @param string $taskName
92
     */
93
    protected function processVariant(
94
        LoggerInterface $logger,
95
        SolrIndex $indexInstance,
96
        $state,
97
        $class,
98
        $includeSubclasses,
99
        $batchSize,
100
        $taskName
101
    ) {
102
        // Set state
103
        SearchVariant::activate_state($state);
104
105
        // Count records
106
        $query = $class::get();
107
        if (!$includeSubclasses) {
108
            $query = $query->filter('ClassName', $class);
109
        }
110
        $total = $query->count();
111
112
        // Skip this variant if nothing to process, or if there are no records
113
        if ($total == 0 || $indexInstance->variantStateExcluded($state)) {
114
            // Remove all records in the current state, since there are no groups to process
115
            $logger->info("Clearing all records of type {$class} in the current state: " . json_encode($state));
116
            $this->clearRecords($indexInstance, $class);
117
            return;
118
        }
119
120
        // For each group, run processing
121
        $groups = (int)(($total + $batchSize - 1) / $batchSize);
122
        for ($group = 0; $group < $groups; $group++) {
123
            $this->processGroup($logger, $indexInstance, $state, $class, $groups, $group, $taskName);
124
        }
125
    }
126
127
    /**
128
     * Initiate the processing of a single group
129
     *
130
     * @param LoggerInterface $logger
131
     * @param SolrIndex $indexInstance Index instance
132
     * @param array $state Variant state
133
     * @param string $class Class to index
134
     * @param int $groups Total groups
135
     * @param int $group Index of group to process
136
     * @param string $taskName Name of task script to run
137
     */
138
    abstract protected function processGroup(
139
        LoggerInterface $logger,
140
        SolrIndex $indexInstance,
141
        $state,
142
        $class,
143
        $groups,
144
        $group,
145
        $taskName
146
    );
147
148
    /**
149
     * Explicitly invoke the process that performs the group
150
     * processing. Can be run either by a background task or a queuedjob.
151
     *
152
     * Does not commit changes to the index, so this must be controlled externally.
153
     *
154
     * @param LoggerInterface $logger
155
     * @param SolrIndex $indexInstance
156
     * @param array $state
157
     * @param string $class
158
     * @param int $groups
159
     * @param int $group
160
     */
161
    public function runGroup(
162
        LoggerInterface $logger,
163
        SolrIndex $indexInstance,
164
        $state,
165
        $class,
166
        $groups,
167
        $group
168
    ) {
169
        // Set time limit and state
170
        Environment::increaseTimeLimitTo();
171
        SearchVariant::activate_state($state);
172
        $logger->info("Adding $class");
173
174
        // Prior to adding these records to solr, delete existing solr records
175
        $this->clearRecords($indexInstance, $class, $groups, $group);
176
177
        // Process selected records in this class
178
        $items = $this->getRecordsInGroup($indexInstance, $class, $groups, $group);
179
        $processed = array();
180
        foreach ($items as $item) {
181
            $processed[] = $item->ID;
182
183
            // By this point, obsolete classes/states have been removed in processVariant
184
            // and obsolete records have been removed in clearRecords
185
            $indexInstance->add($item);
186
            $item->destroy();
187
        }
188
        $logger->info("Updated ".implode(',', $processed));
189
190
        // This will slow down things a tiny bit, but it is done so that we don't timeout to the database during a reindex
191
        DB::query('SELECT 1');
192
193
        $logger->info("Done");
194
    }
195
196
    /**
197
     * Gets the datalist of records in the given group in the current state
198
     *
199
     * Assumes that the desired variant state is in effect.
200
     *
201
     * @param SolrIndex $indexInstance
202
     * @param string $class
203
     * @param int $groups
204
     * @param int $group
205
     * @return DataList
206
     */
207
    protected function getRecordsInGroup(SolrIndex $indexInstance, $class, $groups, $group)
208
    {
209
        // Generate filtered list of local records
210
        $baseClass = DataObject::getSchema()->baseDataClass($class);
211
        $items = DataList::create($class)
0 ignored issues
show
Bug introduced by
$class of type string is incompatible with the type array expected by parameter $args of SilverStripe\View\ViewableData::create(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

211
        $items = DataList::create(/** @scrutinizer ignore-type */ $class)
Loading history...
212
            ->where(sprintf(
213
                '"%s"."ID" %% \'%d\' = \'%d\'',
214
                DataObject::getSchema()->tableName($baseClass),
215
                intval($groups),
216
                intval($group)
217
            ))
218
            ->sort("ID");
219
220
        // Add child filter
221
        $classes = $indexInstance->getClasses();
222
        $options = $classes[$class];
223
        if (!$options['include_children']) {
224
            $items = $items->filter('ClassName', $class);
225
        }
226
227
        return $items;
228
    }
229
230
    /**
231
     * Clear all records of the given class in the current state ONLY.
232
     *
233
     * Optionally delete from a given group (where the group is defined as the ID % total groups)
234
     *
235
     * @param SolrIndex $indexInstance Index instance
236
     * @param string $class Class name
237
     * @param int $groups Number of groups, if clearing from a striped group
238
     * @param int $group Group number, if clearing from a striped group
239
     */
240
    protected function clearRecords(SolrIndex $indexInstance, $class, $groups = null, $group = null)
241
    {
242
        // Clear by classname
243
        $conditions = array("+(ClassHierarchy:{$class})");
244
245
        // If grouping, delete from this group only
246
        if ($groups) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $groups of type null|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
247
            $conditions[] = "+_query_:\"{!frange l={$group} u={$group}}mod(ID, {$groups})\"";
248
        }
249
250
        // Also filter by state (suffix on document ID)
251
        $query = new SearchQuery();
252
        SearchVariant::with($class)
253
            ->call('alterQuery', $query, $indexInstance);
254
        if ($query->isfiltered()) {
255
            $conditions = array_merge($conditions, $indexInstance->getFiltersComponent($query));
256
        }
257
258
        // Invoke delete on index
259
        $deleteQuery = implode(' ', $conditions);
260
        $indexInstance
261
            ->getService()
262
            ->deleteByQuery($deleteQuery);
263
    }
264
}
265