Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 39 and 401]

   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/>.
  16  
  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   */
  24  
  25  namespace search_solr;
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  require_once($CFG->dirroot . '/lib/filelib.php');
  30  
  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 {
  42  
  43      /**
  44       * @var stdClass
  45       */
  46      protected $config = null;
  47  
  48      /**
  49       * cUrl instance.
  50       * @var \curl
  51       */
  52      protected $curl = null;
  53  
  54      /**
  55       * An engine instance.
  56       * @var engine
  57       */
  58      protected $engine = null;
  59  
  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          }
  71  
  72          if (empty($this->config->server_hostname) || empty($this->config->indexname)) {
  73              throw new \moodle_exception('missingconfig', 'search_solr');
  74          }
  75  
  76          $this->engine = $engine ?? new engine();
  77          $this->curl = $this->engine->get_curl_object();
  78  
  79          // HTTP headers.
  80          $this->curl->setHeader('Content-type: application/json');
  81      }
  82  
  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() {
  89  
  90          $status = $this->engine->is_server_configured();
  91          if ($status !== true) {
  92              return $status;
  93          }
  94  
  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          }
 102  
 103          return true;
 104      }
 105  
 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();
 114  
 115          // Field id is already there.
 116          unset($fields['id']);
 117  
 118          $this->check_index();
 119  
 120          $return = $this->add_fields($fields, $checkexisting);
 121  
 122          // Tell the engine we are now using the latest schema version.
 123          $this->engine->record_applied_schema_version(document::SCHEMA_VERSION);
 124  
 125          return $return;
 126      }
 127  
 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();
 136  
 137          // Field id is already there.
 138          unset($fields['id']);
 139  
 140          $this->check_index();
 141          $this->validate_fields($fields, true);
 142      }
 143  
 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() {
 151  
 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      }
 162  
 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) {
 176  
 177          if ($checkexisting) {
 178              // Check that non of them exists.
 179              $this->validate_fields($fields, false);
 180          }
 181  
 182          $url = $this->engine->get_connection_url('/schema');
 183  
 184          // Add all fields.
 185          foreach ($fields as $fieldname => $data) {
 186  
 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']);
 191  
 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));
 203  
 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          }
 212  
 213          return true;
 214      }
 215  
 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;
 226  
 227          foreach ($fields as $fieldname => $data) {
 228              $url = $this->engine->get_connection_url('/schema/fields/' . $fieldname);
 229              $results = $this->curl->get($url);
 230  
 231              if ($this->curl->error) {
 232                  throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $this->curl->error);
 233              }
 234  
 235              if (!$results) {
 236                  throw new \moodle_exception('errorcreatingschema', 'search_solr', '', get_string('nodatafromserver', 'search_solr'));
 237              }
 238              $results = json_decode($results);
 239  
 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              }
 246  
 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.
 254  
 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)) {
 259  
 260                          throw new \moodle_exception('errorcreatingschema', 'search_solr', '',
 261                              get_string('schemafieldautocreated', 'search_solr', $fieldname));
 262  
 263                      } else if ($results->field->type !== $expectedsolrfield ||
 264                              $results->field->multiValued !== false ||
 265                              $results->field->indexed !== $data['indexed'] ||
 266                              $results->field->stored !== $data['stored']) {
 267  
 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      }
 278  
 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) {
 287  
 288          if (!$result) {
 289              throw new \moodle_exception('errorcreatingschema', 'search_solr', '', get_string('nodatafromserver', 'search_solr'));
 290          }
 291  
 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          }
 301  
 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          }
 306  
 307          // It comes as errors when adding fields.
 308          if (!empty($results->errors)) {
 309  
 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          }
 317  
 318      }
 319  
 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;
 328  
 329          $solrversion = $this->engine->get_solr_major_version();
 330  
 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          }
 346  
 347          return $type;
 348      }
 349  }