1<?php
2
3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4// See the LICENCE file in the repository root for full licence text.
5
6namespace App\Console\Commands;
7
8use App\Libraries\Elasticsearch\Es;
9use App\Libraries\Elasticsearch\Indexing;
10use App\Models\ArtistTrack;
11use App\Models\Beatmapset;
12use App\Models\Forum\Post;
13use App\Models\User;
14use Illuminate\Console\Command;
15
16class EsIndexDocuments extends Command
17{
18 const ALLOWED_TYPES = [
19 'artist_tracks' => [ArtistTrack::class],
20 'beatmapsets' => [Beatmapset::class],
21 'posts' => [Post::class],
22 'users' => [User::class],
23 ];
24
25 /**
26 * The name and signature of the console command.
27 *
28 * @var string
29 */
30 protected $signature = 'es:index-documents {--types=} {--inplace} {--cleanup}';
31
32 /**
33 * The console command description.
34 *
35 * @var string
36 */
37 protected $description = 'Indexes documents into Elasticsearch.';
38
39 protected $cleanup;
40 protected $inplace;
41 protected $groups;
42
43 /**
44 * Execute the console command.
45 *
46 * @return mixed
47 */
48 public function handle()
49 {
50 $this->readOptions();
51
52 $oldIndices = [];
53 foreach ($this->groups as $name) {
54 $type = static::ALLOWED_TYPES[$name][0];
55 $oldIndices[] = Indexing::getOldIndices($type::esIndexName());
56 }
57
58 $oldIndices = array_flatten($oldIndices);
59
60 $continue = $this->starterMessage($oldIndices);
61 if (!$continue) {
62 $this->error('User aborted!');
63 return;
64 }
65
66 $start = time();
67
68 foreach ($this->groups as $name) {
69 $this->indexGroup($name);
70 }
71
72 $this->finish($oldIndices);
73 $this->warn("\nIndexing completed in ".(time() - $start).'s');
74 }
75
76 protected function finish(array $oldIndices)
77 {
78 if (!$this->inplace && $this->cleanup) {
79 foreach ($oldIndices as $index) {
80 $this->warn("Removing '{$index}'...");
81 Indexing::deleteIndex($index);
82 }
83 }
84 }
85
86 private function indexGroup($name)
87 {
88 $types = collect(static::ALLOWED_TYPES[$name]);
89
90 $allSame = $types->every(function ($type) use ($types) {
91 return $type::esIndexName() === $types->first()::esIndexName();
92 });
93
94 if (!$allSame) {
95 $this->error("All types in group {$name} must have the same index.");
96
97 return [];
98 }
99
100 $first = $types->first();
101 $alias = $first::esIndexName();
102 $indexName = $this->inplace ? $alias : $first::esTimestampedIndexName();
103 $pretext = $this->inplace ? 'In-place indexing' : 'Indexing';
104
105 foreach ($types as $type) {
106 $bar = $this->output->createProgressBar();
107
108 $this->info("{$pretext} {$type} into {$indexName}");
109
110 if (!$this->inplace && $type === $first) {
111 // create new index if the first type for this index, otherwise
112 // index in place.
113 $type::esIndexIntoNew(Es::CHUNK_SIZE, $indexName, function ($progress) use ($bar) {
114 $bar->setProgress($progress);
115 });
116 } else {
117 $options = ['index' => $indexName];
118 $type::esReindexAll(Es::CHUNK_SIZE, 0, $options, function ($progress) use ($bar) {
119 $bar->setProgress($progress);
120 });
121 }
122
123 $bar->finish();
124 $this->line("\n");
125 }
126
127 if ($alias !== $indexName) {
128 $this->info("Aliasing {$alias} to {$indexName}");
129 Indexing::updateAlias($alias, $indexName);
130 $this->line('');
131 }
132 }
133
134 protected function readOptions()
135 {
136 $this->inplace = $this->option('inplace');
137 $this->cleanup = $this->option('cleanup');
138
139 if ($this->option('types')) {
140 $types = explode(',', $this->option('types'));
141 $this->groups = [];
142 foreach ($types as $type) {
143 if (array_key_exists($type, static::ALLOWED_TYPES)) {
144 $this->groups[] = $type;
145 }
146 }
147 } else {
148 $this->groups = array_keys(static::ALLOWED_TYPES);
149 }
150 }
151
152 protected function starterMessage(array $oldIndices)
153 {
154 if ($this->inplace) {
155 $this->warn('Running in-place reindex.');
156 $confirmMessage = 'This will reindex in-place (schemas must match)';
157 } else {
158 $this->warn('Running index transfer.');
159
160 if ($this->cleanup) {
161 $this->warn(
162 "The following indices will be deleted on completion!\n"
163 .implode("\n", $oldIndices)
164 );
165 }
166
167 $confirmMessage = 'This will create new indices';
168 }
169
170 return $this->option('no-interaction') || $this->confirm("{$confirmMessage}, begin indexing?", true);
171 }
172}