Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

   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       * @throws \moodle_exception
  64       * @return void
  65       */
  66      public function __construct() {
  67          if (!$this->config = get_config('search_solr')) {
  68              throw new \moodle_exception('missingconfig', 'search_solr');
  69          }
  70  
  71          if (empty($this->config->server_hostname) || empty($this->config->indexname)) {
  72              throw new \moodle_exception('missingconfig', 'search_solr');
  73          }
  74  
  75          $this->engine = new engine();
  76          $this->curl = $this->engine->get_curl_object();
  77  
  78          // HTTP headers.
  79          $this->curl->setHeader('Content-type: application/json');
  80      }
  81  
  82      /**
  83       * Can setup be executed against the configured server.
  84       *
  85       * @return true|string True or error message.
  86       */
  87      public function can_setup_server() {
  88  
  89          $status = $this->engine->is_server_configured();
  90          if ($status !== true) {
  91              return $status;
  92          }
  93  
  94          // At this stage we know that the server is properly configured with a valid host:port and indexname.
  95          // We're not too concerned about repeating the SolrClient::system() call (already called in
  96          // is_server_configured) because this is just a setup script.
  97          if ($this->engine->get_solr_major_version() < 5) {
  98              // Schema setup script only available for 5.0 onwards.
  99              return get_string('schemasetupfromsolr5', 'search_solr');
 100          }
 101  
 102          return true;
 103      }
 104  
 105      /**
 106       * Setup solr stuff required by moodle.
 107       *
 108       * @param  bool $checkexisting Whether to check if the fields already exist or not
 109       * @return bool
 110       */
 111      public function setup($checkexisting = true) {
 112          $fields = \search_solr\document::get_default_fields_definition();
 113  
 114          // Field id is already there.
 115          unset($fields['id']);
 116  
 117          $this->check_index();
 118  
 119          $return = $this->add_fields($fields, $checkexisting);
 120  
 121          // Tell the engine we are now using the latest schema version.
 122          $this->engine->record_applied_schema_version(document::SCHEMA_VERSION);
 123  
 124          return $return;
 125      }
 126  
 127      /**
 128       * Checks the schema is properly set up.
 129       *
 130       * @throws \moodle_exception
 131       * @return void
 132       */
 133      public function validate_setup() {
 134          $fields = \search_solr\document::get_default_fields_definition();
 135  
 136          // Field id is already there.
 137          unset($fields['id']);
 138  
 139          $this->check_index();
 140          $this->validate_fields($fields, true);
 141      }
 142  
 143      /**
 144       * Checks if the index is ready, triggers an exception otherwise.
 145       *
 146       * @throws \moodle_exception
 147       * @return void
 148       */
 149      protected function check_index() {
 150  
 151          // Check that the server is available and the index exists.
 152          $url = $this->engine->get_connection_url('/select?wt=json');
 153          $result = $this->curl->get($url);
 154          if ($this->curl->error) {
 155              throw new \moodle_exception('connectionerror', 'search_solr');
 156          }
 157          if ($this->curl->info['http_code'] === 404) {
 158              throw new \moodle_exception('connectionerror', 'search_solr');
 159          }
 160      }
 161  
 162      /**
 163       * Adds the provided fields to Solr schema.
 164       *
 165       * Intentionally separated from create(), it can be called to add extra fields.
 166       * fields separately.
 167       *
 168       * @throws \coding_exception
 169       * @throws \moodle_exception
 170       * @param  array $fields \core_search\document::$requiredfields format
 171       * @param  bool $checkexisting Whether to check if the fields already exist or not
 172       * @return bool
 173       */
 174      protected function add_fields($fields, $checkexisting = true) {
 175  
 176          if ($checkexisting) {
 177              // Check that non of them exists.
 178              $this->validate_fields($fields, false);
 179          }
 180  
 181          $url = $this->engine->get_connection_url('/schema');
 182  
 183          // Add all fields.
 184          foreach ($fields as $fieldname => $data) {
 185  
 186              if (!isset($data['type']) || !isset($data['stored']) || !isset($data['indexed'])) {
 187                  throw new \coding_exception($fieldname . ' does not define all required field params: type, stored and indexed.');
 188              }
 189              $type = $this->doc_field_to_solr_field($data['type']);
 190  
 191              // Changing default multiValued value to false as we want to match values easily.
 192              $params = array(
 193                  'add-field' => array(
 194                      'name' => $fieldname,
 195                      'type' => $type,
 196                      'stored' => $data['stored'],
 197                      'multiValued' => false,
 198                      'indexed' => $data['indexed']
 199                  )
 200              );
 201              $results = $this->curl->post($url, json_encode($params));
 202  
 203              // We only validate if we are interested on it.
 204              if ($checkexisting) {
 205                  if ($this->curl->error) {
 206                      throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $this->curl->error);
 207                  }
 208                  $this->validate_add_field_result($results);
 209              }
 210          }
 211  
 212          return true;
 213      }
 214  
 215      /**
 216       * Checks if the schema existing fields are properly set, triggers an exception otherwise.
 217       *
 218       * @throws \moodle_exception
 219       * @param array $fields
 220       * @param bool $requireexisting Require the fields to exist, otherwise exception.
 221       * @return void
 222       */
 223      protected function validate_fields(&$fields, $requireexisting = false) {
 224          global $CFG;
 225  
 226          foreach ($fields as $fieldname => $data) {
 227              $url = $this->engine->get_connection_url('/schema/fields/' . $fieldname);
 228              $results = $this->curl->get($url);
 229  
 230              if ($this->curl->error) {
 231                  throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $this->curl->error);
 232              }
 233  
 234              if (!$results) {
 235                  throw new \moodle_exception('errorcreatingschema', 'search_solr', '', get_string('nodatafromserver', 'search_solr'));
 236              }
 237              $results = json_decode($results);
 238  
 239              if ($requireexisting && !empty($results->error) && $results->error->code === 404) {
 240                  $a = new \stdClass();
 241                  $a->fieldname = $fieldname;
 242                  $a->setupurl = $CFG->wwwroot . '/search/engine/solr/setup_schema.php';
 243                  throw new \moodle_exception('errorvalidatingschema', 'search_solr', '', $a);
 244              }
 245  
 246              // The field should not exist so we only accept 404 errors.
 247              if (empty($results->error) || (!empty($results->error) && $results->error->code !== 404)) {
 248                  if (!empty($results->error)) {
 249                      throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $results->error->msg);
 250                  } else {
 251                      // All these field attributes are set when fields are added through this script and should
 252                      // be returned and match the defined field's values.
 253  
 254                      $expectedsolrfield = $this->doc_field_to_solr_field($data['type']);
 255                      if (empty($results->field) || !isset($results->field->type) ||
 256                              !isset($results->field->multiValued) || !isset($results->field->indexed) ||
 257                              !isset($results->field->stored)) {
 258  
 259                          throw new \moodle_exception('errorcreatingschema', 'search_solr', '',
 260                              get_string('schemafieldautocreated', 'search_solr', $fieldname));
 261  
 262                      } else if ($results->field->type !== $expectedsolrfield ||
 263                              $results->field->multiValued !== false ||
 264                              $results->field->indexed !== $data['indexed'] ||
 265                              $results->field->stored !== $data['stored']) {
 266  
 267                          throw new \moodle_exception('errorcreatingschema', 'search_solr', '',
 268                              get_string('schemafieldautocreated', 'search_solr', $fieldname));
 269                      } else {
 270                          // The field already exists and it is properly defined, no need to create it.
 271                          unset($fields[$fieldname]);
 272                      }
 273                  }
 274              }
 275          }
 276      }
 277  
 278      /**
 279       * Checks that the field results do not contain errors.
 280       *
 281       * @throws \moodle_exception
 282       * @param string $results curl response body
 283       * @return void
 284       */
 285      protected function validate_add_field_result($result) {
 286  
 287          if (!$result) {
 288              throw new \moodle_exception('errorcreatingschema', 'search_solr', '', get_string('nodatafromserver', 'search_solr'));
 289          }
 290  
 291          $results = json_decode($result);
 292          if (!$results) {
 293              if (is_scalar($result)) {
 294                  $errormsg = $result;
 295              } else {
 296                  $errormsg = json_encode($result);
 297              }
 298              throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $errormsg);
 299          }
 300  
 301          // It comes as error when fetching fields data.
 302          if (!empty($results->error)) {
 303              throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $results->error);
 304          }
 305  
 306          // It comes as errors when adding fields.
 307          if (!empty($results->errors)) {
 308  
 309              // We treat this error separately.
 310              $errorstr = '';
 311              foreach ($results->errors as $error) {
 312                  $errorstr .= implode(', ', $error->errorMessages);
 313              }
 314              throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $errorstr);
 315          }
 316  
 317      }
 318  
 319      /**
 320       * Returns the solr field type from the document field type string.
 321       *
 322       * @param string $datatype
 323       * @return string
 324       */
 325      private function doc_field_to_solr_field($datatype) {
 326          $type = $datatype;
 327  
 328          $solrversion = $this->engine->get_solr_major_version();
 329  
 330          switch($datatype) {
 331              case 'text':
 332                  $type = 'text_general';
 333                  break;
 334              case 'int':
 335                  if ($solrversion >= 7) {
 336                      $type = 'pint';
 337                  }
 338                  break;
 339              case 'tdate':
 340                  if ($solrversion >= 7) {
 341                      $type = 'pdate';
 342                  }
 343                  break;
 344          }
 345  
 346          return $type;
 347      }
 348  }