MDL-68729 Search: Allow query on one server while indexing another
[moodle.git] / search / engine / solr / classes / schema.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Solr schema manipulation manager.
19  *
20  * @package   search_solr
21  * @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
22  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 namespace search_solr;
27 defined('MOODLE_INTERNAL') || die();
29 require_once($CFG->dirroot . '/lib/filelib.php');
31 /**
32  * Schema class to interact with Solr schema.
33  *
34  * At the moment it only implements create which should be enough for a basic
35  * moodle configuration in Solr.
36  *
37  * @package   search_solr
38  * @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
39  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40  */
41 class schema {
43     /**
44      * @var stdClass
45      */
46     protected $config = null;
48     /**
49      * cUrl instance.
50      * @var \curl
51      */
52     protected $curl = null;
54     /**
55      * An engine instance.
56      * @var engine
57      */
58     protected $engine = null;
60     /**
61      * Constructor.
62      *
63      * @param engine $engine Optional engine parameter, if not specified then one will be created
64      * @throws \moodle_exception
65      * @return void
66      */
67     public function __construct(engine $engine = null) {
68         if (!$this->config = get_config('search_solr')) {
69             throw new \moodle_exception('missingconfig', 'search_solr');
70         }
72         if (empty($this->config->server_hostname) || empty($this->config->indexname)) {
73             throw new \moodle_exception('missingconfig', 'search_solr');
74         }
76         $this->engine = $engine ?? new engine();
77         $this->curl = $this->engine->get_curl_object();
79         // HTTP headers.
80         $this->curl->setHeader('Content-type: application/json');
81     }
83     /**
84      * Can setup be executed against the configured server.
85      *
86      * @return true|string True or error message.
87      */
88     public function can_setup_server() {
90         $status = $this->engine->is_server_configured();
91         if ($status !== true) {
92             return $status;
93         }
95         // At this stage we know that the server is properly configured with a valid host:port and indexname.
96         // We're not too concerned about repeating the SolrClient::system() call (already called in
97         // is_server_configured) because this is just a setup script.
98         if ($this->engine->get_solr_major_version() < 5) {
99             // Schema setup script only available for 5.0 onwards.
100             return get_string('schemasetupfromsolr5', 'search_solr');
101         }
103         return true;
104     }
106     /**
107      * Setup solr stuff required by moodle.
108      *
109      * @param  bool $checkexisting Whether to check if the fields already exist or not
110      * @return bool
111      */
112     public function setup($checkexisting = true) {
113         $fields = \search_solr\document::get_default_fields_definition();
115         // Field id is already there.
116         unset($fields['id']);
118         $this->check_index();
120         $return = $this->add_fields($fields, $checkexisting);
122         // Tell the engine we are now using the latest schema version.
123         $this->engine->record_applied_schema_version(document::SCHEMA_VERSION);
125         return $return;
126     }
128     /**
129      * Checks the schema is properly set up.
130      *
131      * @throws \moodle_exception
132      * @return void
133      */
134     public function validate_setup() {
135         $fields = \search_solr\document::get_default_fields_definition();
137         // Field id is already there.
138         unset($fields['id']);
140         $this->check_index();
141         $this->validate_fields($fields, true);
142     }
144     /**
145      * Checks if the index is ready, triggers an exception otherwise.
146      *
147      * @throws \moodle_exception
148      * @return void
149      */
150     protected function check_index() {
152         // Check that the server is available and the index exists.
153         $url = $this->engine->get_connection_url('/select?wt=json');
154         $result = $this->curl->get($url);
155         if ($this->curl->error) {
156             throw new \moodle_exception('connectionerror', 'search_solr');
157         }
158         if ($this->curl->info['http_code'] === 404) {
159             throw new \moodle_exception('connectionerror', 'search_solr');
160         }
161     }
163     /**
164      * Adds the provided fields to Solr schema.
165      *
166      * Intentionally separated from create(), it can be called to add extra fields.
167      * fields separately.
168      *
169      * @throws \coding_exception
170      * @throws \moodle_exception
171      * @param  array $fields \core_search\document::$requiredfields format
172      * @param  bool $checkexisting Whether to check if the fields already exist or not
173      * @return bool
174      */
175     protected function add_fields($fields, $checkexisting = true) {
177         if ($checkexisting) {
178             // Check that non of them exists.
179             $this->validate_fields($fields, false);
180         }
182         $url = $this->engine->get_connection_url('/schema');
184         // Add all fields.
185         foreach ($fields as $fieldname => $data) {
187             if (!isset($data['type']) || !isset($data['stored']) || !isset($data['indexed'])) {
188                 throw new \coding_exception($fieldname . ' does not define all required field params: type, stored and indexed.');
189             }
190             $type = $this->doc_field_to_solr_field($data['type']);
192             // Changing default multiValued value to false as we want to match values easily.
193             $params = array(
194                 'add-field' => array(
195                     'name' => $fieldname,
196                     'type' => $type,
197                     'stored' => $data['stored'],
198                     'multiValued' => false,
199                     'indexed' => $data['indexed']
200                 )
201             );
202             $results = $this->curl->post($url, json_encode($params));
204             // We only validate if we are interested on it.
205             if ($checkexisting) {
206                 if ($this->curl->error) {
207                     throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $this->curl->error);
208                 }
209                 $this->validate_add_field_result($results);
210             }
211         }
213         return true;
214     }
216     /**
217      * Checks if the schema existing fields are properly set, triggers an exception otherwise.
218      *
219      * @throws \moodle_exception
220      * @param array $fields
221      * @param bool $requireexisting Require the fields to exist, otherwise exception.
222      * @return void
223      */
224     protected function validate_fields(&$fields, $requireexisting = false) {
225         global $CFG;
227         foreach ($fields as $fieldname => $data) {
228             $url = $this->engine->get_connection_url('/schema/fields/' . $fieldname);
229             $results = $this->curl->get($url);
231             if ($this->curl->error) {
232                 throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $this->curl->error);
233             }
235             if (!$results) {
236                 throw new \moodle_exception('errorcreatingschema', 'search_solr', '', get_string('nodatafromserver', 'search_solr'));
237             }
238             $results = json_decode($results);
240             if ($requireexisting && !empty($results->error) && $results->error->code === 404) {
241                 $a = new \stdClass();
242                 $a->fieldname = $fieldname;
243                 $a->setupurl = $CFG->wwwroot . '/search/engine/solr/setup_schema.php';
244                 throw new \moodle_exception('errorvalidatingschema', 'search_solr', '', $a);
245             }
247             // The field should not exist so we only accept 404 errors.
248             if (empty($results->error) || (!empty($results->error) && $results->error->code !== 404)) {
249                 if (!empty($results->error)) {
250                     throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $results->error->msg);
251                 } else {
252                     // All these field attributes are set when fields are added through this script and should
253                     // be returned and match the defined field's values.
255                     $expectedsolrfield = $this->doc_field_to_solr_field($data['type']);
256                     if (empty($results->field) || !isset($results->field->type) ||
257                             !isset($results->field->multiValued) || !isset($results->field->indexed) ||
258                             !isset($results->field->stored)) {
260                         throw new \moodle_exception('errorcreatingschema', 'search_solr', '',
261                             get_string('schemafieldautocreated', 'search_solr', $fieldname));
263                     } else if ($results->field->type !== $expectedsolrfield ||
264                             $results->field->multiValued !== false ||
265                             $results->field->indexed !== $data['indexed'] ||
266                             $results->field->stored !== $data['stored']) {
268                         throw new \moodle_exception('errorcreatingschema', 'search_solr', '',
269                             get_string('schemafieldautocreated', 'search_solr', $fieldname));
270                     } else {
271                         // The field already exists and it is properly defined, no need to create it.
272                         unset($fields[$fieldname]);
273                     }
274                 }
275             }
276         }
277     }
279     /**
280      * Checks that the field results do not contain errors.
281      *
282      * @throws \moodle_exception
283      * @param string $results curl response body
284      * @return void
285      */
286     protected function validate_add_field_result($result) {
288         if (!$result) {
289             throw new \moodle_exception('errorcreatingschema', 'search_solr', '', get_string('nodatafromserver', 'search_solr'));
290         }
292         $results = json_decode($result);
293         if (!$results) {
294             if (is_scalar($result)) {
295                 $errormsg = $result;
296             } else {
297                 $errormsg = json_encode($result);
298             }
299             throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $errormsg);
300         }
302         // It comes as error when fetching fields data.
303         if (!empty($results->error)) {
304             throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $results->error);
305         }
307         // It comes as errors when adding fields.
308         if (!empty($results->errors)) {
310             // We treat this error separately.
311             $errorstr = '';
312             foreach ($results->errors as $error) {
313                 $errorstr .= implode(', ', $error->errorMessages);
314             }
315             throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $errorstr);
316         }
318     }
320     /**
321      * Returns the solr field type from the document field type string.
322      *
323      * @param string $datatype
324      * @return string
325      */
326     private function doc_field_to_solr_field($datatype) {
327         $type = $datatype;
329         $solrversion = $this->engine->get_solr_major_version();
331         switch($datatype) {
332             case 'text':
333                 $type = 'text_general';
334                 break;
335             case 'int':
336                 if ($solrversion >= 7) {
337                     $type = 'pint';
338                 }
339                 break;
340             case 'tdate':
341                 if ($solrversion >= 7) {
342                     $type = 'pdate';
343                 }
344                 break;
345         }
347         return $type;
348     }