<?php

class XenES_Search_SourceHandler_ElasticSearch extends XenForo_Search_SourceHandler_Abstract
{
	protected $_indexName = null;
	protected $_bulkInserts = array();

	const SPLIT_CHAR_RANGES = '\x00-\x21\x23-\x26\x28\x29\x2B\x2C\x2F\x3A-\x40\x5B-\x5E\x60\x7B-\x7F';

	/**
	 * Get the correct index name
	 */
	public function __construct()
	{
		$this->_indexName = XenForo_Application::get('options')->elasticSearchIndex;
		if (!$this->_indexName)
		{
			$this->_indexName = XenForo_Application::get('config')->db->dbname;
		}
	}

	/**
	 * (non-PHPdoc)
	 * @see XenForo_Search_SourceHandler_Abstract::supportsRelevance()
	 */
	public function supportsRelevance()
	{
		return true;
	}

	/**
	 * (non-PHPdoc)
	 * @see XenForo_Search_SourceHandler_Abstract::insertIntoIndex()
	 */
	public function insertIntoIndex($contentType, $contentId, $title, $message, $itemDate, $userId, $discussionId = 0, array $metadata = array())
	{
		$record = array_merge($metadata, array(
			'title' => $title,
			'message' => $message,
			'date' => $itemDate,
			'user' => $userId,
			'discussion_id' => $discussionId
		));

		if ($this->_isRebuild)
		{
			$this->_bulkInserts[$contentType][$contentId] = $record;
			if (count($this->_bulkInserts[$contentType]) > 2500)
			{
				$result = XenES_Api::indexBulk($this->_indexName, $contentType, $this->_bulkInserts[$contentType]);
				if (!$this->isIndexSuccessful($result, $failed))
				{
					$this->_triggerFailedIndexError($result, $failed);
				}
				unset($this->_bulkInserts[$contentType]);
			}
		}
		else
		{
			$result = XenES_Api::index($this->_indexName, $contentType, $contentId, $record);
			if (!$this->isIndexSuccessful($result, $failed))
			{
				$this->_triggerFailedIndexError($result, $failed, true);
				$this->_logFailedIndex('index', $contentType, $contentId, $record);
			}
		}
	}

	public function finalizeRebuildSet()
	{
		foreach ($this->_bulkInserts AS $contentType => $results)
		{
			$result = XenES_Api::indexBulk($this->_indexName, $contentType, $results);
			if (!$this->isIndexSuccessful($result, $failed))
			{
				$this->_triggerFailedIndexError($result, $failed);
			}
			unset($this->_bulkInserts[$contentType]);
		}
		$this->_bulkInserts = array();
	}

	/**
	 * (non-PHPdoc)
	 * @see XenForo_Search_SourceHandler_Abstract::updateIndex()
	 */
	public function updateIndex($contentType, $contentId, array $fieldUpdates)
	{
		$indexer = new XenForo_Search_Indexer();
		$indexer->quickIndex($contentType, $contentId);
	}

	/**
	 * (non-PHPdoc)
	 * @see XenForo_Search_SourceHandler_Abstract::deleteFromIndex()
	 */
	public function deleteFromIndex($contentType, array $contentIds)
	{
		if (count($contentIds) > 1)
		{
			$result = XenES_Api::deleteBulk($this->_indexName, $contentType, $contentIds);
		}
		else
		{
			$result = XenES_Api::delete($this->_indexName, $contentType, reset($contentIds));
		}

		if (!$this->isIndexSuccessful($result, $failed))
		{
			$this->_triggerFailedIndexError($result, $failed, true);
			foreach ($failed AS $fail)
			{
				$this->_logFailedIndex('delete', $fail[0], $fail[1]);
			}
		}
	}

	/**
	 * Deletes the entire search index or a particular part of it.
	 *
	 * @param string|null $contentType If specified, only deletes the index for this type
	 */
	public function deleteIndex($contentType = null)
	{
		/** @var $esModel XenES_Model_Elasticsearch */
		$esModel = XenForo_Model::create('XenES_Model_Elasticsearch');

		if ($contentType)
		{
			$esModel->optimizeMapping($contentType);
		}
		else
		{
			$esModel->recreateIndex();
		}
	}

	/**
	 * (non-PHPdoc)
	 * @see XenForo_Search_SourceHandler_Abstract::executeSearch()
	 */
	public function executeSearch($searchQuery, $titleOnly, array $processedConstraints, array $orderParts,
		$groupByDiscussionType, $maxResults, XenForo_Search_DataHandler_Abstract $typeHandler = null)
	{
		if ($maxResults < 1)
		{
			$maxResults = 100;
		}
		$maxResults = intval($maxResults);

		// search terms
		$searchQuery = $this->parseQuery($searchQuery);
		if ($searchQuery !== '')
		{
			$queryDsl = array(
				'query_string' => array(
					'query' => $searchQuery,
					'fields' => $titleOnly ? array('title') : array('title^3', 'message'),
					'default_operator' => 'and'
				)
			);
		}
		else
		{
			$queryDsl = array(
				'match_all' => new stdClass()
			);
		}

		// constraints
		$dsl = array();

		foreach ($processedConstraints AS $constraintName => $constraint)
		{
			if ($this->_processConstraint($dsl, $constraintName, $constraint))
			{
				unset($processedConstraints[$constraintName]);
			}
		}

		// DSL 'filtered' if necessary
		if (empty($dsl['query']['filtered']['filter']))
		{
			$dsl['query'] = $queryDsl;
		}
		else
		{
			$dsl['query']['filtered']['query'] = $queryDsl;
		}

		$firstOrder = reset($orderParts);
		if ($firstOrder && $firstOrder[0] == 'search_index' && $firstOrder[1] == 'relevance')
		{
			$weightedRelevance = XenForo_Application::getOptions()->esRecencyWeightedRelevance;
			if ($weightedRelevance['enabled'])
			{
				$version = XenES_Api::version();
				if ($version >= '1.3')
				{
					$language = 'groovy';
				}
				else
				{
					$language = 'mvel';
				}

				// exponential decay with the specified halflife
				if (XenForo_Application::getOptions()->esDynamicScripting)
				{
					if ($language == 'groovy')
					{
						$script = "_score / pow(2.0F, min(10.0F * halflife, abs(now - doc['date'].value)) / halflife)";
					}
					else
					{
						$script = "_score / pow(2.0f, min(10.0f * halflife, abs(now - doc['date'].value + .0f)) / (halflife + .0f))";
					}
				}
				else
				{
					$script = 'xf-date-weighted';
				}

				if ($version >= '1.0')
				{
					$dsl['query'] = array(
						'function_score' => array(
							'boost_mode' => 'replace',
							'query' => $dsl['query'],
							'script_score' => array(
								'params' => array(
									'now' => XenForo_Application::$time,
									'halflife' => 86400 * $weightedRelevance['halfLifeDays']
								),
								'lang' => $language,
								'script' => $script
							)
						)
					);
				}
				else
				{
					$dsl['query'] = array(
						'custom_score' => array(
							'query' => $dsl['query'],
							'params' => array(
								'now' => XenForo_Application::$time,
								'halflife' => 86400 * $weightedRelevance['halfLifeDays']
							),
							'script' => $script
						)
					);
				}
			}
		}

		$sqlTables = array();
		$sqlSort = array();
		$sqlConditions = array();

		// ordering
		foreach ($orderParts AS $order)
		{
			if ($order[0] == 'search_index')
			{
				// sorting on field in the search index - can use ES
				if ($order[1] == 'item_date')
				{
					$order[1] = 'date';
				}
				else if ($order[1] == 'relevance')
				{
					$order[1] = '_score';
				}
				$dsl['sort'][] = array($order[1] => $order[2]);
			}
			else
			{
				$sqlSort[] = $order;
				$sqlTables[] = $order[0];
			}
		}

		$dsl['size'] = $maxResults;
		$dsl['fields'] = array('discussion_id', 'user', 'date');

		if ($groupByDiscussionType)
		{
			$dsl['size'] *= 4; // post result filtering
		}

		if (!empty($processedConstraints))
		{
			$dsl['size'] *= 4; // in sql filtering

			foreach ($processedConstraints AS $constraintName => $constraint)
			{
				$this->_processSqlConstraint($constraint, $sqlConditions, $sqlTables);
			}
		}

		$response = XenES_Api::search($this->_indexName, $dsl);
		if (!$response || !isset($response->hits, $response->hits->hits))
		{
			$this->_logSearchResponseError($response, true);
			throw new XenForo_Exception(new XenForo_Phrase('search_could_not_be_completed_try_again_later'), true);
		}

		$hits = $response->hits->hits;
		if ($sqlTables && $typeHandler)
		{
			// need to return more entries if grouping by discussion to be filtered later
			$hits = $this->_getSqlLimitedHits(
				$hits, $sqlTables, $sqlConditions, $sqlSort,
				($groupByDiscussionType ? $maxResults * 4 : $maxResults), $typeHandler
			);
		}

		$results = $this->_getResultsFromHits($hits, $groupByDiscussionType);
		$results = array_slice($results, 0, $maxResults);

		return $results;
	}

	protected function _getResultsFromHits(array $hits, $groupByDiscussionType)
	{
		$results = array();
		$grouped = array();

		foreach ($hits AS $result)
		{
			if ($groupByDiscussionType && isset($result->fields))
			{
				$groupId = $this->_readFieldFromResult($result, 'discussion_id', 0);
				if ($groupId && !isset($grouped[$groupId]))
				{
					$grouped[$groupId] = $groupId;
				}
			}
			else
			{
				$results[] = array(
					XenForo_Model_Search::CONTENT_TYPE => $result->_type,
					XenForo_Model_Search::CONTENT_ID => intval($result->_id)
				);
			}
		}

		if ($groupByDiscussionType)
		{
			foreach ($grouped AS $groupId)
			{
				$results[] = array(
					XenForo_Model_Search::CONTENT_TYPE => $groupByDiscussionType,
					XenForo_Model_Search::CONTENT_ID => $groupId
				);
			}
		}

		return $results;
	}

	protected function _readFieldFromResult($result, $fieldId, $fallback = null)
	{
		if (!is_object($result) || !isset($result->fields) || !isset($result->fields->$fieldId))
		{
			return $fallback;
		}

		$value = $result->fields->$fieldId;
		if (is_array($value))
		{
			if (count($value))
			{
				return reset($value);
			}
			else
			{
				return $fallback;
			}
		}
		else
		{
			return $value;
		}
	}

	protected function _getSqlLimitedHits(array $hits, array $sqlTables, array $sqlConditions, array $sqlSort,
		$maxResults, XenForo_Search_DataHandler_Abstract $typeHandler
	)
	{
		$sqlTables = array_flip($sqlTables);
		$joinStructures = $typeHandler->getJoinStructures($sqlTables);

		$joins = array();
		foreach ($joinStructures AS $tableAlias => $joinStructure)
		{
			list($relationshipTable, $relationshipField) = $joinStructure['relationship'];
			$joins[] = "INNER JOIN $joinStructure[table] AS $tableAlias ON
				($tableAlias.$joinStructure[key] = $relationshipTable.$relationshipField)";
		}

		$orderFields = array();
		foreach ($sqlSort AS $order)
		{
			list($orderTable, $orderField, $orderDirection) = $order;
			$orderFields[] = "$orderTable.$orderField $orderDirection";
		}
		if ($orderFields)
		{
			$orderClause = 'ORDER BY ' . implode(', ', $orderFields) . ', search_index.item_date DESC';
		}
		else
		{
			$orderClause = 'ORDER BY search_index.hit_position';
		}

		$whereClause = ($sqlConditions ? 'WHERE (' . implode(') AND (', $sqlConditions) . ')' : '');

		$db = $this->_getDb();
		$db->query('
			CREATE TEMPORARY TABLE xf_search_index_temp (
				hit_position int unsigned not null,
				content_type varchar(25) not null,
				content_id int unsigned not null,
				user_id int unsigned not null,
				item_date int unsigned not null,
				discussion_id int unsigned not null
			)
		');

		$bulkInsert = array();
		$bulkInsertLength = 0;
		$insertQuery = '
			INSERT INTO xf_search_index_temp
				(hit_position, content_type, content_id, user_id, item_date, discussion_id)
			VALUES
				%s
		';

		foreach ($hits AS $hitPosition => $hit)
		{
			$row = '(' . $db->quote($hitPosition) . ', ' . $db->quote($hit->_type) . ', ' . $db->quote($hit->_id)
				. ', ' . $db->quote($this->_readFieldFromResult($hit, 'user', 0))
				. ', ' . $db->quote($this->_readFieldFromResult($hit, 'date', 0))
				. ', ' . $db->quote($this->_readFieldFromResult($hit, 'discussion_id', 0)) . ')';

			$bulkInsert[] = $row;
			$bulkInsertLength += strlen($row);

			if ($bulkInsertLength > 500000)
			{
				$db->query(sprintf($insertQuery, implode(',', $bulkInsert)));

				$bulkInsert = array();
				$bulkInsertLength = 0;
			}
		}

		if ($bulkInsert)
		{
			$db->query(sprintf($insertQuery, implode(',', $bulkInsert)));
			$bulkInsert = false;
		}

		$hitPositions = $db->fetchCol($db->limit("
			SELECT search_index.hit_position
			FROM xf_search_index_temp AS search_index
			" . implode("\n", $joins) . "
			$whereClause
			$orderClause
		", $maxResults));

		$filteredHits = array();
		foreach ($hitPositions AS $hitPos)
		{
			$filteredHits[] = $hits[$hitPos];
		}

		return $filteredHits;
	}

	/**
	 * (non-PHPdoc)
	 * @see XenForo_Search_SourceHandler_Abstract::executeSearchByUserId()
	 */
	public function executeSearchByUserId($userId, $maxDate, $maxResults)
	{
		if ($maxResults < 1)
		{
			$maxResults = 100;
		}
		$maxResults = intval($maxResults);

		$dsl = array(
			'size' => $maxResults,
			'sort' => array(array('date' => 'desc')),
			'fields' => array(),
		);

		if ($maxDate)
		{
			$dsl['query'] = array(
				'filtered' => array(
					'query' => array(
						'query_string' => array('query' => "user:$userId")
					),
					'filter' => array(
						array('numeric_range' => array('date' => array('lt' => $maxDate)))
					)
				)
			);
		}
		else
		{
			$dsl['query'] = array(
				'query_string' => array('query' => "user:$userId")
			);
		}

		$results = array();
		$response = XenES_Api::search($this->_indexName, $dsl);

		if ($response && isset($response->hits))
		{
			foreach ($response->hits->hits AS $result)
			{
				$results[] = array(
					XenForo_Model_Search::CONTENT_TYPE => $result->_type,
					XenForo_Model_Search::CONTENT_ID => intval($result->_id)
				);
			}
		}
		else
		{
			$this->_logSearchResponseError($response, true);
			throw new XenForo_Exception(new XenForo_Phrase('search_could_not_be_completed_try_again_later'), true);
		}

		return $results;
	}

	/**
	 * Gets the general order clauses for a search.
	 *
	 * @param string $order User-requested order
	 *
	 * @return array Structured order clause, array of arrays. Child array keys: 0 = table alias, 1 = field, 2 = dir (asc/desc)
	 */
	public function getGeneralOrderClause($order)
	{
		if ($order == 'relevance')
		{
			return array(
				array('search_index', 'relevance', 'desc'),
				array('search_index', 'item_date', 'desc')
			);
		}
		else
		{
			return array(
				array('search_index', 'item_date', 'desc')
			);
		}
	}

	public function parseQuery($query)
	{
		$query = str_replace(array('(', ')'), ' ', trim($query)); // don't support grouping yet

		preg_match_all('/
			(?<=[' . self::SPLIT_CHAR_RANGES .'\-\+\|]|^)
			(?P<modifier>\-|\+|\||)
			[' . self::SPLIT_CHAR_RANGES .']*
			(?P<term>"(?P<quoteTerm>[^"]+)"|[^' . self::SPLIT_CHAR_RANGES .'\-\+\|]+)
		/ix', $query, $matches, PREG_SET_ORDER);

		$output = array();
		$i = 0;

		$haveWords = false;
		$invalidWords = array();

		foreach ($matches AS $match)
		{
			$iStart = $i;

			if ($match['modifier'] == '|' && $i == 0)
			{
				$match['modifier'] = '';
			}

			if (!empty($match['quoteTerm']))
			{
				$term = preg_replace('/[' . self::SPLIT_CHAR_RANGES . ']/', ' ', $match['quoteTerm']);
				$quoted = true;
			}
			else
			{
				$term = str_replace('"', ' ', $match['term']); // unmatched quotes
				$term = preg_replace('/^(AND|OR|NOT)$/', '', $term); // words have special meaning
				$quoted = false;
			}

			$term = trim($term);
			$words = $this->splitWords($term);

			if ($term === '')
			{
				continue;
			}

			foreach ($words AS $word)
			{
				if ($word === '')
				{
					continue;
				}

				if (in_array($word, self::$stopWords))
				{
					$invalidWords[] = $word;
				}
				else
				{
					$haveWords = true;
				}
			}

			$output[$i] = array($match['modifier'], ($quoted ? "\"$term\"" : $term));
			$i++;
		}

		if (!$haveWords && $invalidWords)
		{
			$this->error(new XenForo_Phrase('search_could_not_be_completed_because_search_keywords_were_too'), 'keywords');
		}
		else if ($invalidWords)
		{
			$this->warning(
				new XenForo_Phrase(
					'following_words_were_not_included_in_your_search_x',
					array('words' => implode(', ', $invalidWords))
				), 'keywords'
			);
		}

		$newQuery = '';
		foreach ($output AS $part)
		{
			if ($part[0] == '|')
			{
				$part[0] = 'OR ';
			}

			$newQuery .= ' ' . $part[0] . $part[1];
		}

		return trim($newQuery);
	}

	/**
	 * Split words by the delimiters.
	 *
	 * @param string $words
	 *
	 * @return array
	 */
	public function splitWords($words)
	{
		// delimiters: 0 - 38, 40, 41, 43 - 47, 58 - 64, 91 - 94, 96, 123 - 127
		return preg_split('/[' . self::SPLIT_CHAR_RANGES . ']/', $words, -1, PREG_SPLIT_NO_EMPTY);
	}

	/**
	 * Process a search constraint into DSL
	 *
	 * @param array $dsl
	 * @param string $constraintName
	 * @param array $constraint
	 *
	 * @return boolean
	 */
	protected function _processConstraint(array &$dsl, $constraintName, array $constraint)
	{
		if (isset($constraint['metadata']))
		{
			return $this->_processMetaDataConstraint($dsl, $constraint['metadata'][0], $constraint['metadata'][1]);
		}
		else if (isset($constraint['query']))
		{
			return $this->_processQueryConstraint($dsl, $constraintName, $constraint['query']);
		}

		return false;
	}

	/**
	 * Process a metadata search constraint into DSL
	 *
	 * @param array $dsl
	 * @param string $metaDataField
	 * @param array $values
	 *
	 * @return boolean
	 */
	protected function _processMetaDataConstraint(array &$dsl, $metaDataField, $values)
	{
		if (!is_array($values))
		{
			$values = array($values);
		}
		foreach ($values AS $key => $value)
		{
			if ($value === '')
			{
				unset($values[$key]);
			}
		}
		if (!$values)
		{
			return false;
		}

		if ($metaDataField == 'content')
		{
			if (count($values) > 1)
			{
				$ors = array();
				foreach ($values AS $value)
				{
					$ors[]['type']['value'] = $value;
				}

				if ($ors)
				{
					$dsl['query']['filtered']['filter']['and'][]['or'] = $ors;
				}
			}
			else
			{
				$dsl['query']['filtered']['filter']['and'][]['type']['value'] = reset($values);
			}
		}
		else if (count($values) > 1)
		{
			$dsl['query']['filtered']['filter']['and'][]['terms'][$metaDataField] = $values;
		}
		else
		{
			$dsl['query']['filtered']['filter']['and'][]['term'][$metaDataField] = reset($values);
		}

		return true;
	}

	/**
	 * Process a query constraint into DSL
	 *
	 * @param array $dsl
	 * @param string $constraintName
	 * @param array $constraint
	 *
	 * @return boolean
	 */
	protected function _processQueryConstraint(array &$dsl, $constraintName, array $constraint)
	{
		if ($constraint[0] == 'search_index' && $constraint[1] == 'item_date')
		{
			switch ($constraint[2]) // operator
			{
				case '>':  $operator = 'gt';  break;
				case '<':  $operator = 'lt';  break;
				case '>=': $operator = 'gte'; break;
				case '<=': $operator = 'lte'; break;
				default: return false;
			}

			$dsl['query']['filtered']['filter']['and'][]['range']['date'][$operator] = $constraint[3];
			return true;
		}

		return false;
	}

	protected function _processSqlConstraint(array $constraint, array &$sqlConditions, array &$sqlTables)
	{
		if (empty($constraint['query']) || $constraint['query'][0] == 'search_index')
		{
			return;
		}

		list($table, $field, $operator, $value) = $constraint['query'];
		$db = $this->_getDb();

		if ($operator == '=' && is_array($value))
		{
			$sqlConditions[] = "$table.$field IN (" . $db->quote($value) . ")";
		}
		else
		{
			if (!is_scalar($value))
			{
				$value = strval($value);
			}
			$sqlConditions[] = "$table.$field $operator " . $db->quote($value);
		}

		$sqlTables[] = $table;
	}

	/**
	 * Translate XenForo query operators into ElasticSearch operators
	 *
	 * @param string $operator
	 *
	 * @return string|false
	 */
	protected function _getRangeOperator($operator)
	{
		switch ($operator)
		{
			case '>':
				return 'gt';
			case '<':
				return 'lt';
			case '>=':
				return 'gte';
			case '<=':
				return 'lte';
		}

		return false;
	}

	protected function _logSearchResponseError($response, $rollback = false, $extraMessage = '')
	{
		if ($response && is_string($response))
		{
			$e = new XenForo_Exception(trim($extraMessage . ' Elasticsearch error: ' . $response));
			XenForo_Error::logException($e, $rollback);
		}
		else if ($response && !empty($response->error))
		{
			$e = new XenForo_Exception(trim($extraMessage . ' Elasticsearch error: ' . $response->error));
			XenForo_Error::logException($e, $rollback);
		}
		else if (!$response)
		{
			$e = new XenForo_Exception(trim('Elasticsearch server returned no response. Is it running? ' . $extraMessage));
			XenForo_Error::logException($e, $rollback);
		}
	}

	public function isIndexSuccessful($result, &$failed = array())
	{
		$failed = array();

		if (!$result)
		{
			return false;
		}
		if (isset($result->errors) && !$result->errors)
		{
			return false;
		}

		if (!empty($result->error))
		{
			if (!empty($result->_type) && !empty($result->_id))
			{
				$failed[] = array($result->_type, $result->_id, $result->error);
			}
			else
			{
				$failed[] = array('', '', $result->error);
			}
		}
		else if (!empty($result->items))
		{
			// bulk action
			foreach ($result->items AS $item)
			{
				if (!empty($item->index))
				{
					$record = $item->index;
				}
				else if (!empty($item->delete))
				{
					$record = $item->delete;
				}
				else
				{
					continue;
				}

				if (!empty($record->error))
				{
					$failed[] = array($record->_type, $record->_id, $record->error);
				}
			}
		}

		return empty($failed);
	}

	protected function _assertIndexSuccessful($result, $contentType = null, $contentId = null)
	{
		$ok = $this->isIndexSuccessful($result, $failed);

		if (!$ok)
		{
			if ($failed)
			{
				list($contentType, $contentId, $error) = reset($failed);
			}
			else
			{
				$error = false;
			}
			$this->_logSearchResponseError($error, $this->_isRebuild, "Elasticsearch indexing failed for $contentType-$contentId");
			if ($this->_isRebuild)
			{
				if ($error === false)
				{
					throw new XenForo_Exception(new XenForo_Phrase('no_response_returned_elasticsearch_is_it_running'), true);
				}
				else
				{
					throw new XenForo_Exception(new XenForo_Phrase('elasticsearch_indexing_failed_error_log_more_details'), true);
				}
			}
		}
	}

	protected function _triggerFailedIndexError($result, $failed, $requeued = false)
	{
		if ($failed)
		{
			list($contentType, $contentId, $error) = reset($failed);
		}
		else if (!$result)
		{
			$error = false;
			$contentType = null;
			$contentId = null;
			$requeued = false;
		}
		else
		{
			// no error
			return;
		}

		if ($contentType)
		{
			$errorPrefix = "Elasticsearch indexing failed for $contentType-$contentId"
				. ($requeued ? ", requeued for indexing" : '');
		}
		else
		{
			$errorPrefix = "Elasticsearch indexing failed";
		}

		$this->_logSearchResponseError($error, $this->_isRebuild, $errorPrefix);

		if ($this->_isRebuild)
		{
			if ($error === false)
			{
				throw new XenForo_Exception(new XenForo_Phrase('no_response_returned_elasticsearch_is_it_running'), true);
			}
			else
			{
				throw new XenForo_Exception(new XenForo_Phrase('elasticsearch_indexing_failed_error_log_more_details'), true);
			}
		}
	}

	protected function _logFailedIndex($action, $contentType, $contentId, array $record = null)
	{
		/** @var XenES_Model_Elasticsearch $esModel */
		$esModel = XenForo_Model::create('XenES_Model_Elasticsearch');
		return $esModel->logFailedIndex($action, $contentType, $contentId, $record, 0);
	}

	public static $stopWords = array(
		'a', 'an', 'and', 'are', 'as', 'be', 'but', 'by', 'for',
		'if', 'in', 'into', 'is', 'it', 'no', 'not', 'of', 'on',
		'or', 'such', 'that', 'the', 'their', 'then', 'there',
		'these', 'they', 'this', 'to', 'was', 'will', 'truth'
	);
}
