Version 5.0.0 beta 1

This commit is contained in:
Neo committed 2025-12-19 16:27:35 -08:00
1 parent 25ddeb65d6
commit 15c7beabc5
6736 files changed
+627902 -497943

No files matched your search

+7 -3
View File
@@ -11,15 +11,19 @@
namespace IPS\Content\Search\Elastic;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use DomainException;
use function defined;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Elasticsearch Search Exception Class
*/
class _Exception extends \DomainException
class Exception extends DomainException
{
}
+200 -160
View File
@@ -11,16 +11,49 @@
namespace IPS\Content\Search\Elastic;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use Exception;
use IPS\Content\Comment;
use IPS\Content\Item;
use IPS\Content\Search\Index as SearchIndex;
use IPS\Content\Search\Elastic\Exception as ElasticException;
use IPS\Content;
use IPS\DateTime;
use IPS\Db;
use IPS\Http\Request\Exception as RequestException;
use IPS\Http\Request\Curl;
use IPS\Http\Response;
use IPS\Http\Url;
use IPS\IPS;
use IPS\Log;
use IPS\Member;
use IPS\Settings;
use OutOfRangeException;
use RuntimeException;
use StdClass;
use function count;
use function defined;
use function get_class;
use function in_array;
use function intval;
use function is_array;
use function is_null;
use function strtolower;
use function substr;
use const IPS\ELASTICSEARCH_PASSWORD;
use const IPS\ELASTICSEARCH_USER;
use const IPS\LONG_REQUEST_TIMEOUT;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Elasticsearch Search Index
*/
class _Index extends \IPS\Content\Search\Index
class Index extends SearchIndex
{
/**
* @brief Elasticsearch version requirements
@@ -31,15 +64,15 @@ class _Index extends \IPS\Content\Search\Index
/**
* @brief The server URL
*/
protected $url;
protected Url $url;
/**
* Constructor
*
* @param \IPS\Http\Url $url The server URL
* @param Url $url The server URL
* @return void
*/
public function __construct( \IPS\Http\Url $url )
public function __construct( Url $url )
{
$this->url = $url;
}
@@ -49,21 +82,21 @@ class _Index extends \IPS\Content\Search\Index
*
* @return void
*/
public function init()
public function init(): void
{
try
{
$analyzer = \IPS\Settings::i()->search_elastic_analyzer;
$analyzer = Settings::i()->search_elastic_analyzer;
$settings = array(
'max_result_window' => \IPS\Settings::i()->search_index_maxresults
'max_result_window' => Settings::i()->search_index_maxresults
);
if ( $analyzer === 'custom' )
{
$settings['analysis'] = json_decode( '{' . \IPS\Settings::i()->search_elastic_custom_analyzer . '}', TRUE );
$settings['analysis'] = json_decode( '{' . Settings::i()->search_elastic_custom_analyzer . '}', TRUE );
$analyzer = key( $settings['analysis']['analyzer'] );
}
\IPS\Content\Search\Elastic\Index::request( $this->url )->delete();
Index::request( $this->url )->delete();
$definition = array(
'settings' => $settings,
@@ -117,38 +150,38 @@ class _Index extends \IPS\Content\Search\Index
try
{
$response = \IPS\Content\Search\Elastic\Index::request( $this->url->setQueryString( 'include_type_name', 'true' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( $definition ) );
$response = Index::request( $this->url->setQueryString( 'include_type_name', 'true' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( $definition ) );
if( $response->httpResponseCode != 200 )
{
throw new \RuntimeException;
throw new RuntimeException;
}
}
catch( \Exception $e )
catch( Exception )
{
$response = \IPS\Content\Search\Elastic\Index::request( $this->url )->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( $definition ) );
$response = Index::request( $this->url )->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( $definition ) );
}
}
catch ( \Exception $e )
catch ( Exception $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
Log::log( $e, 'elasticsearch' );
}
}
/**
* Get index data
*
* @param \IPS\Content\Searchable $object Item to add
* @param Content $object Item to add
* @return array|NULL
*/
public function indexData( \IPS\Content\Searchable $object )
public function indexData( Content $object ): array|NULL
{
if ( $indexData = parent::indexData( $object ) )
{
$indexData['index_permissions'] = explode( ',', $indexData['index_permissions'] );
$indexData['index_is_last_comment'] = (bool) $indexData['index_is_last_comment'];
if ( $object instanceof \IPS\Content\Item )
if ( $object instanceof Item )
{
$indexData = array_merge( $indexData, $this->metaData( $object ) );
}
@@ -166,10 +199,10 @@ class _Index extends \IPS\Content\Search\Index
/**
* Index an item
*
* @param \IPS\Content\Searchable $object Item to add
* @param Content $object Item to add
* @return void
*/
public function index( \IPS\Content\Searchable $object )
public function index( Content $object ): void
{
if ( $indexData = $this->indexData( $object ) )
{
@@ -190,16 +223,16 @@ class _Index extends \IPS\Content\Search\Index
try
{
$r = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $object ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get()->decodeJson();
$r = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $object ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get()->decodeJson();
if ( $r['found'] )
{
$existingData = $r['_source'];
$existingIndexId = $r['_id'];
}
}
catch( \Exception $e ) { }
catch( Exception ) { }
if ( $object instanceof \IPS\Content\Comment and $existingIndexId and $existingData['index_is_last_comment'] and $indexData['index_item_id'] and $indexData['index_hidden'] !== 0 )
if ( $object instanceof Comment and $existingIndexId and $existingData['index_is_last_comment'] and $indexData['index_is_last_comment'] and $indexData['index_item_id'] and $indexData['index_hidden'] !== 0 )
{
/* We do not allow hidden or needing approval comments to become flagged as the last comment as this means users without hidden view permission never see the item in an item only stream */
$indexData['index_is_last_comment'] = false;
@@ -213,7 +246,7 @@ class _Index extends \IPS\Content\Search\Index
}
/* Insert into index */
$r = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $object ) ), \IPS\LONG_REQUEST_TIMEOUT )
$r = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $object ) ), LONG_REQUEST_TIMEOUT )
->setHeaders( array( 'Content-Type' => 'application/json' ) )
->put( json_encode( $indexData ) );
@@ -228,24 +261,24 @@ class _Index extends \IPS\Content\Search\Index
}
/* Views / Comments / Reviews */
if ( $object instanceof \IPS\Content\Item )
if ( $object instanceof Item )
{
$item = $object;
}
elseif ( $object instanceof \IPS\Content\Comment )
elseif ( $object instanceof Comment )
{
$item = $object->item();
}
$this->rebuildMetaData( $item );
}
catch ( \IPS\Http\Request\Exception $e )
catch ( RequestException $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
Log::log( $e, 'elasticsearch' );
}
catch ( \IPS\Content\Search\Elastic\Exception $e )
catch ( ElasticException $e )
{
\IPS\Log::log( $e, 'elasticsearch_response_error' );
Log::log( $e, 'elasticsearch_response_error' );
}
}
}
@@ -256,16 +289,16 @@ class _Index extends \IPS\Content\Search\Index
*
* @return void
*/
public function clearTasks()
public function clearTasks(): void
{
try
{
/* This request *intentionally* goes to _tasks and not (ourpath)/_tasks */
$response = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( '/_tasks' ), \IPS\LONG_REQUEST_TIMEOUT )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get();
$response = Index::request( $this->url->setPath( '/_tasks' ), LONG_REQUEST_TIMEOUT )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get();
if( $error = $this->getResponseError( $response ) )
{
throw new \IPS\Content\Search\Elastic\Exception( $error['type'] . ' ' . $error['type'] );
throw new ElasticException( $error['type'] . ' ' . $error['type'] );
}
$response = $response->decodeJson();
@@ -286,45 +319,53 @@ class _Index extends \IPS\Content\Search\Index
continue;
}
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( '/.tasks/task' . $taskId ), \IPS\LONG_REQUEST_TIMEOUT )->setHeaders( array( 'Content-Type' => 'application/json' ) )->delete();
Index::request( $this->url->setPath( '/.tasks/task' . $taskId ), LONG_REQUEST_TIMEOUT )->setHeaders( array( 'Content-Type' => 'application/json' ) )->delete();
}
}
}
catch ( \IPS\Content\Search\Elastic\Exception $e )
catch ( ElasticException $e )
{
\IPS\Log::log( $e, 'elasticsearch_response_error' );
Log::log( $e, 'elasticsearch_response_error' );
}
catch( \Exception $e )
catch( Exception $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
Log::log( $e, 'elasticsearch' );
}
}
/**
* Get the comment / review counts for an item
*
* @param \IPS\Content\Searchable $item The content item
* @return void
* @param Item $item The content item
* @return array|null
*/
protected function metaData( $item )
protected function metaData( Item $item ): ?array
{
$databaseColumnId = $item::$databaseColumnId;
if( !Content\Search\SearchContent::isSearchable( $item ) )
{
return null;
}
$databaseColumnId = $item::$databaseColumnId;
/* @var array $databaseColumnMap */
$participants = array( $item->mapped('author') );
if ( isset( $item::$commentClass ) )
{
/* @var Comment $commentClass */
$commentClass = $item::$commentClass;
$participants += iterator_to_array( \IPS\Db::i()->select( 'DISTINCT ' . $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['author'], $commentClass::$databaseTable, array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['item'] . '=?', $item->$databaseColumnId ) ) );
$participants += iterator_to_array( Db::i()->select( 'DISTINCT ' . $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['author'], $commentClass::$databaseTable, array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['item'] . '=?', $item->$databaseColumnId ) ) );
}
if ( isset( $item::$reviewClass ) )
{
/* @var Content\Review $reviewClass */
$reviewClass = $item::$reviewClass;
$participants += iterator_to_array( \IPS\Db::i()->select( 'DISTINCT ' . $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['author'], $reviewClass::$databaseTable, array( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['item'] . '=?', $item->$databaseColumnId ) ) );
$participants += iterator_to_array( Db::i()->select( 'DISTINCT ' . $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['author'], $reviewClass::$databaseTable, array( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['item'] . '=?', $item->$databaseColumnId ) ) );
}
$participants = array_values( array_unique( $participants ) );
$isSolved = 0;
if ( \IPS\IPS::classUsesTrait( $item, 'IPS\Content\Solvable' ) and $item->isSolved() )
if ( IPS::classUsesTrait( $item, 'IPS\Content\Solvable' ) and $item->isSolved() )
{
$isSolved = 1;
}
@@ -342,13 +383,13 @@ class _Index extends \IPS\Content\Search\Index
/**
* Rebuild the comment / review counts for an item
*
* @param \IPS\Content\Searchable $item The content item
* @param Item $item The content item
* @return void
*/
protected function rebuildMetaData( $item )
protected function rebuildMetaData( Item $item ): void
{
$databaseColumnId = $item::$databaseColumnId;
$class = \get_class( $item );
$class = get_class( $item );
$classes = array( $class );
if ( isset( $class::$commentClass ) )
{
@@ -365,24 +406,22 @@ class _Index extends \IPS\Content\Search\Index
$params = array();
foreach ( $this->metaData( $item ) as $k => $v )
{
if ( \is_array( $v ) )
$updates[] = "ctx._source.{$k} = params.param_{$k};";
if ( is_array( $v ) )
{
$updates[] = "ctx._source.{$k} = params.param_{$k};";
$params[ 'param_' . $k ] = ( $k !== 'index_tags' ? array_map( 'intval', $v ) : $v );
}
elseif ( \is_null( $v ) )
elseif ( is_null( $v ) )
{
$updates[] = "ctx._source.{$k} = params.param_{$k};";
$params['param_' . $k ] = null;
}
else
{
$updates[] = "ctx._source.{$k} = params.param_{$k};";
$params['param_' . $k ] = \intval( $v );
$params['param_' . $k ] = intval( $v );
}
}
$r = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) ), \IPS\LONG_REQUEST_TIMEOUT )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
$r = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => Settings::i()->search_index_maxresults ) ), LONG_REQUEST_TIMEOUT )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'script' => array(
'source' => implode( ' ', $updates ),
'lang' => 'painless',
@@ -406,44 +445,45 @@ class _Index extends \IPS\Content\Search\Index
)
) ) );
}
catch ( \IPS\Http\Request\Exception $e )
catch ( RequestException $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
Log::log( $e, 'elasticsearch' );
}
}
/**
* Retrieve the search ID for an item
*
* @param \IPS\Content\Searchable $object Item to add
* @return string
* @param Content $object Item to add
* @return mixed
*/
public function getIndexId( \IPS\Content\Searchable $object ): string
public function getIndexId( Content $object ): mixed
{
$databaseColumnId = $object::$databaseColumnId;
return \strtolower( str_replace( '\\', '_', \substr( \get_class( $object ), 4 ) ) ) . '-' . $object->$databaseColumnId;
return strtolower( str_replace( '\\', '_', substr( get_class( $object ), 4 ) ) ) . '-' . $object->$databaseColumnId;
}
/**
* Remove item
*
* @param \IPS\Content\Searchable $object Item to remove
* @return void
* @param Content $object Item to remove
* @return string
*/
public function removeFromSearchIndex( \IPS\Content\Searchable $object )
public function removeFromSearchIndex( Content $object ): void
{
try
{
$class = \get_class( $object );
$class = get_class( $object );
$idColumn = $class::$databaseColumnId;
$this->directIndexRemoval( $class, $object->$idColumn );
if ( !( $object instanceof \IPS\Content\Item ) )
if ( !( $object instanceof Item ) )
{
$this->rebuildMetaData( $object->item() );
$itemClass = \get_class( $object->item() );
/* @var Item $itemClass */
$itemClass = get_class( $object->item() );
$itemIdColumn = $itemClass::$databaseColumnId;
$classes = array( $itemClass );
@@ -456,12 +496,12 @@ class _Index extends \IPS\Content\Search\Index
$classes[] = $itemClass::$reviewClass;
}
$this->resetLastComment( $classes, $object->item()->$itemIdColumn, $object->$idColumn, $this->getFirstCommentId( $object ) );
$this->resetLastComment( $classes, $object->item()->$itemIdColumn, $object->$idColumn, null, $this->getFirstCommentId( $object ) );
}
}
catch ( \IPS\Http\Request\Exception $e )
catch ( RequestException $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
Log::log( $e, 'elasticsearch' );
}
}
@@ -472,19 +512,19 @@ class _Index extends \IPS\Content\Search\Index
* @param int $id ID
* @return void
*/
public function directIndexRemoval( $class, $id )
public function directIndexRemoval( string $class, int $id ): void
{
try
{
$indexId = \strtolower( str_replace( '\\', '_', \substr( $class, 4 ) ) ) . '-' . $id;
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/' . $indexId ) )->delete();
$indexId = strtolower( str_replace( '\\', '_', substr( $class, 4 ) ) ) . '-' . $id;
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/' . $indexId ) )->delete();
if ( is_subclass_of( $class, 'IPS\Content\Item' ) )
{
if ( isset( $class::$commentClass ) )
{
$commentClass = $class::$commentClass;
$response = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
$response = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'query' => array(
'bool' => array(
'must' => array(
@@ -507,7 +547,7 @@ class _Index extends \IPS\Content\Search\Index
if ( isset( $class::$reviewClass ) )
{
$reviewClass = $class::$reviewClass;
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'query' => array(
'bool' => array(
'must' => array(
@@ -529,9 +569,9 @@ class _Index extends \IPS\Content\Search\Index
}
}
}
catch ( \IPS\Http\Request\Exception $e )
catch ( RequestException $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
Log::log( $e, 'elasticsearch' );
}
}
@@ -543,7 +583,7 @@ class _Index extends \IPS\Content\Search\Index
* @param int|NULL $authorId The author ID to delete, or NULL
* @return void
*/
public function removeClassFromSearchIndex( $class, $containerId=NULL, $authorId=NULL )
public function removeClassFromSearchIndex( string $class, int|null $containerId=null, int|null $authorId=null ): void
{
try
{
@@ -579,13 +619,13 @@ class _Index extends \IPS\Content\Search\Index
);
}
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'query' => $query
) ) );
}
else
{
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'query' => array(
'term' => array(
'index_class' => $class
@@ -594,9 +634,9 @@ class _Index extends \IPS\Content\Search\Index
) ) );
}
}
catch ( \IPS\Http\Request\Exception $e )
catch ( RequestException $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
Log::log( $e, 'elasticsearch' );
}
}
@@ -615,7 +655,7 @@ class _Index extends \IPS\Content\Search\Index
* @param bool $addAuthorToPermissions If true, the index_author_id will be added to $newPermissions - used when changing the permissions for a node which allows access only to author's items
* @return void
*/
public function massUpdate( $class, $containerId = NULL, $itemId = NULL, $newPermissions = NULL, $newHiddenStatus = NULL, $newContainer = NULL, $authorId = NULL, $newItemId = NULL, $newItemAuthorId = NULL, $addAuthorToPermissions = FALSE )
public function massUpdate( string $class, int|null $containerId = null, int|null $itemId = null, string|null $newPermissions = null, int|null $newHiddenStatus = null, int|null $newContainer = null, int|null $authorId = null, int|null $newItemId = null, int|null $newItemAuthorId = null, bool $addAuthorToPermissions = FALSE ): void
{
try
{
@@ -660,16 +700,17 @@ class _Index extends \IPS\Content\Search\Index
if ( $newContainer )
{
$updates[] = "ctx._source.index_container_id = params.params_indexcontainer;";
$params['params_indexcontainer'] = \intval( $newContainer );
if ( $itemClass = ( \in_array( 'IPS\Content\Item', class_parents( $class ) ) ? $class : $class::$itemClass ) and $containerClass = $itemClass::$containerNodeClass and \IPS\IPS::classUsesTrait( $containerClass, 'IPS\Content\ClubContainer' ) and $clubIdColumn = $containerClass::clubIdColumn() )
$params['params_indexcontainer'] = $newContainer;
/* @var Item $itemClass */
if ( $itemClass = ( in_array( 'IPS\Content\Item', class_parents( $class ) ) ? $class : $class::$itemClass ) and $containerClass = $itemClass::$containerNodeClass and IPS::classUsesTrait( $containerClass, 'IPS\Content\ClubContainer' ) and $clubIdColumn = $containerClass::clubIdColumn() )
{
try
{
$updates[] = "ctx._source.index_club_id = params.params_indexclub;";
$params['params_indexclub'] = \intval( $containerClass::load( $newContainer )->$clubIdColumn );
$params['params_indexclub'] = intval( $containerClass::load( $newContainer )->$clubIdColumn );
}
catch ( \OutOfRangeException $e )
catch ( OutOfRangeException )
{
$updates[] = "ctx._source.index_club_id = params.params_indexclub;";
$params['params_indexclub'] = null;
@@ -679,17 +720,17 @@ class _Index extends \IPS\Content\Search\Index
if ( $newItemId )
{
$updates[] = "ctx._source.index_item_id = params.params_indexitem;";
$params['params_indexitem'] = \intval( $newItemId );
$params['params_indexitem'] = $newItemId;
}
if ( $newItemAuthorId )
{
$updates[] = "ctx._source.index_item_author = params.params_indexauthor;";
$params['params_indexauthor'] = \intval( $newItemAuthorId );
$params['params_indexauthor'] = $newItemAuthorId;
}
if ( \count( $updates ) )
if ( count( $updates ) )
{
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => Settings::i()->search_index_maxresults ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'script' => array(
'source' => implode( ' ', $updates ),
'lang' => 'painless',
@@ -710,7 +751,7 @@ class _Index extends \IPS\Content\Search\Index
)
);
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => Settings::i()->search_index_maxresults ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'script' => array(
'source' => "ctx._source.index_permissions.add( 'm' + ctx._source.index_author );",
'lang' => 'painless'
@@ -740,11 +781,11 @@ class _Index extends \IPS\Content\Search\Index
);
}
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => Settings::i()->search_index_maxresults ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'script' => array(
'source' => "ctx._source.index_hidden = params.newHiddenStatus;",
'lang' => 'painless',
'params' => array( 'newHiddenStatus' => \intval( $newHiddenStatus ) )
'params' => array( 'newHiddenStatus' => $newHiddenStatus )
),
'query' => array(
'bool' => $conditions
@@ -752,9 +793,9 @@ class _Index extends \IPS\Content\Search\Index
) ) );
}
}
catch ( \IPS\Http\Request\Exception $e )
catch ( RequestException $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
Log::log( $e, 'elasticsearch' );
}
}
@@ -764,15 +805,15 @@ class _Index extends \IPS\Content\Search\Index
* @param array $conditions Conditions
* @return array
*/
public static function convertConditionsToQuery( $conditions )
public static function convertConditionsToQuery( array $conditions ): array
{
if ( \count( $conditions ) == 1 )
if ( count( $conditions ) == 1 )
{
return $conditions[0];
}
elseif ( \count( $conditions ) == 0 )
elseif ( count( $conditions ) == 0 )
{
return array( 'match_all' => new \StdClass );
return array( 'match_all' => new StdClass );
}
else
{
@@ -788,17 +829,16 @@ class _Index extends \IPS\Content\Search\Index
* Update data for the first and last comment after a merge
* Sets index_is_last_comment on the last comment, and, if this is an item where the first comment is indexed rather than the item, sets index_title and index_tags on the first comment
*
* @param \IPS\Content\Item $item The item
* @param Item $item The item
* @return void
*/
public function rebuildAfterMerge( \IPS\Content\Item $item )
public function rebuildAfterMerge( Item $item ): void
{
if ( $item::$commentClass )
{
$firstComment = $item->comments( 1, 0, 'date', 'asc', NULL, FALSE, NULL, NULL, TRUE, FALSE, FALSE );
$lastComment = $item->comments( 1, 0, 'date', 'desc', NULL, FALSE, NULL, NULL, TRUE, FALSE, FALSE );
$idColumn = $item::$databaseColumnId;
$update = array( 'index_is_last_comment' => false );
if ( $item::$firstCommentRequired )
{
@@ -807,7 +847,7 @@ class _Index extends \IPS\Content\Search\Index
try
{
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $item ) . '/_update' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $item ) . '/_update' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'doc' => $update
) ) );
@@ -820,9 +860,9 @@ class _Index extends \IPS\Content\Search\Index
$this->index( $lastComment );
}
}
catch ( \IPS\Http\Request\Exception $e )
catch ( RequestException $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
Log::log( $e, 'elasticsearch' );
}
}
}
@@ -830,16 +870,16 @@ class _Index extends \IPS\Content\Search\Index
/**
* Prune search index
*
* @param \IPS\DateTime|NULL $cutoff The date to delete index records from, or NULL to delete all
* @param DateTime|NULL $cutoff The date to delete index records from, or NULL to delete all
* @return void
*/
public function prune( \IPS\DateTime $cutoff = NULL )
public function prune( DateTime|null $cutoff = NULL ): void
{
if ( $cutoff )
{
try
{
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'query' => array(
'range' => array(
'index_date_updated' => array(
@@ -849,9 +889,9 @@ class _Index extends \IPS\Content\Search\Index
)
) ) );
}
catch ( \IPS\Http\Request\Exception $e )
catch ( RequestException $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
Log::log( $e, 'elasticsearch' );
}
}
else
@@ -859,22 +899,22 @@ class _Index extends \IPS\Content\Search\Index
$this->init();
}
}
/**
* Reset the last comment flag in any given class/index_item_id
*
* @param array $classes The classes (when first post is required, this is typically just \IPS\forums\Topic\Post but for others, it will be both item and comment classes)
* @param int|NULL $indexItemId The index item ID
* @param int|NULL $ignoreId ID to ignore because it is being removed
* @param int|null $firstCommentId The first comment in this item, used only if $firstCommentRequired
* @param array $classes The classes (when first post is required, this is typically just \IPS\forums\Topic\Post but for others, it will be both item and comment classes)
* @param int|NULL $indexItemId The index item ID
* @param int|NULL $ignoreId ID to ignore because it is being removed
* @param int|null $firstCommentId The first comment in this item, used only if $firstCommentRequired
* @return void
*/
public function resetLastComment( $classes, $indexItemId, $ignoreId = NULL, $firstCommentId = NULL )
public function resetLastComment( array $classes, int|null $indexItemId, int|null $ignoreId = null, int|null $firstCommentId = null ): void
{
try
{
/* Remove the flag */
$r = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
$r = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'script' => array(
'source' => "ctx._source.index_is_last_comment = false;",
'lang' => 'painless'
@@ -906,7 +946,7 @@ class _Index extends \IPS\Content\Search\Index
$itemClass = NULL;
foreach ( $classes as $class )
{
if ( \in_array( 'IPS\Content\Item', class_parents( $class ) ) )
if ( in_array( 'IPS\Content\Item', class_parents( $class ) ) )
{
$itemClass = $class;
break;
@@ -931,10 +971,10 @@ class _Index extends \IPS\Content\Search\Index
$where = array( $commentClass::$databaseTable . '.' . $commentClass::$databasePrefix . $commentIdColumn . '<>?', $ignoreId );
}
if ( $lastComment = $item->comments( 1, 0, 'date', 'desc', NULL, FALSE, NULL, $where ) AND $lastComment instanceof \IPS\Content\Searchable )
if ( $lastComment = $item->comments( 1, 0, 'date', 'desc', NULL, FALSE, NULL, $where ) AND Content\Search\SearchContent::isSearchable( $lastComment ) )
{
/* Set that it is the latest comment */
$r = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_update/' . $this->getIndexId( $lastComment ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
$r = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_update/' . $this->getIndexId( $lastComment ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'doc' => array(
'index_is_last_comment' => true
)
@@ -942,13 +982,13 @@ class _Index extends \IPS\Content\Search\Index
/* And set the updated time on the main item (done as _update_by_query because it might not exist if the first comment is required) */
$indexDataForLastComment = $this->indexData( $lastComment );
$r = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
$r = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'script' => array(
'source' => "ctx._source.index_date_updated = params.dateUpdated; ctx._source.index_date_commented = params.dateCommented;",
'lang' => 'painless',
'params' => array(
'dateUpdated' => \intval( $indexDataForLastComment['index_date_updated'] ),
'dateCommented' => \intval( $indexDataForLastComment['index_date_commented'] )
'dateUpdated' => intval( $indexDataForLastComment['index_date_updated'] ),
'dateCommented' => intval( $indexDataForLastComment['index_date_commented'] )
)
),
'query' => array(
@@ -975,12 +1015,12 @@ class _Index extends \IPS\Content\Search\Index
) ) );
}
}
catch ( \OutOfRangeException $e ) {}
catch ( OutOfRangeException ) {}
}
}
catch ( \Exception $e )
catch ( Exception $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
Log::log( $e, 'elasticsearch' );
}
}
@@ -988,10 +1028,10 @@ class _Index extends \IPS\Content\Search\Index
* Given a list of item index IDs, return the ones that a given member has participated in
*
* @param array $itemIndexIds Item index IDs
* @param \IPS\Member $member The member
* @param Member $member The member
* @return array
*/
public function iPostedIn( array $itemIndexIds, \IPS\Member $member )
public function iPostedIn( array $itemIndexIds, Member $member ): array
{
try
{
@@ -1014,7 +1054,7 @@ class _Index extends \IPS\Content\Search\Index
);
/* Get the count */
$count = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
$count = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
'size' => 0,
'query' => $query
) ) )->decodeJson();
@@ -1025,7 +1065,7 @@ class _Index extends \IPS\Content\Search\Index
}
/* Now get the unique item ids */
$results = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
$results = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
'aggs' => array(
'itemIds' => array(
'terms' => array(
@@ -1037,9 +1077,9 @@ class _Index extends \IPS\Content\Search\Index
'query' => $query
) ) )->decodeJson();
}
catch ( \Exception $e )
catch ( Exception $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
Log::log( $e, 'elasticsearch' );
return array();
}
@@ -1059,15 +1099,15 @@ class _Index extends \IPS\Content\Search\Index
* Given a list of "index_class_type_id_hash"s, return the ones that a given member has permission to view
*
* @param array $hashes Item index hashes
* @param \IPS\Member $member The member
* @param Member $member The member
* @param int|NULL $limit Number of results to return
* @return array
*/
public function hashesWithPermission( array $hashes, \IPS\Member $member, $limit = NULL )
public function hashesWithPermission( array $hashes, Member $member, int|null $limit = null ): array
{
try
{
$results = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
$results = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
'query' => array(
'bool' => array(
'filter' => array(
@@ -1092,9 +1132,9 @@ class _Index extends \IPS\Content\Search\Index
'size' => $limit ?: 10 // If we define a limit, use that, otherwise default to 10 which is ElasticSearch's default
) ) )->decodeJson();
}
catch ( \Exception $e )
catch ( Exception $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
Log::log( $e, 'elasticsearch' );
return array();
}
@@ -1112,11 +1152,11 @@ class _Index extends \IPS\Content\Search\Index
*
* @return int|null
*/
public function firstIndexDate()
public function firstIndexDate(): int|NULL
{
try
{
$results = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
$results = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
'size' => 1,
'sort' => array( array( 'index_date_updated' => 'asc' ) )
) ) )->decodeJson();
@@ -1128,9 +1168,9 @@ class _Index extends \IPS\Content\Search\Index
return NULL;
}
catch ( \Exception $e )
catch ( Exception $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
Log::log( $e, 'elasticsearch' );
return NULL;
}
}
@@ -1141,18 +1181,18 @@ class _Index extends \IPS\Content\Search\Index
* @param array $terms Terms
* @return array
*/
public function stemmedTerms( $terms )
public function stemmedTerms( array $terms ): array
{
$analyzer = \IPS\Settings::i()->search_elastic_analyzer;
$analyzer = Settings::i()->search_elastic_analyzer;
if ( $analyzer === 'custom' )
{
$analysisSettings = json_decode( '{' . \IPS\Settings::i()->search_elastic_custom_analyzer . '}', TRUE );
$analysisSettings = json_decode( '{' . Settings::i()->search_elastic_custom_analyzer . '}', TRUE );
$analyzer = key( $analysisSettings['analyzer'] );
}
try
{
$results = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( '/_analyze' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
$results = Index::request( $this->url->setPath( '/_analyze' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
'analyzer' => $analyzer,
'text' => implode( ' ', $terms )
) ) )->decodeJson();
@@ -1169,7 +1209,7 @@ class _Index extends \IPS\Content\Search\Index
return $terms;
}
catch ( \Exception $e )
catch ( Exception )
{
return $terms;
}
@@ -1180,7 +1220,7 @@ class _Index extends \IPS\Content\Search\Index
*
* @return bool
*/
public function supportViewFiltering()
public function supportViewFiltering(): bool
{
return FALSE;
}
@@ -1188,15 +1228,15 @@ class _Index extends \IPS\Content\Search\Index
/**
* Wrapper to account for log in where needed
*
* @param $url
* @param null $timeout
* @return \IPS\Http\Request\Curl|Socket
* @param Url $url
* @param int|null $timeout
* @return Curl
*/
public static function request( $url, $timeout=NULL )
public static function request( Url $url, int|null $timeout=NULL ) : Curl
{
if ( \IPS\ELASTICSEARCH_USER or \IPS\ELASTICSEARCH_PASSWORD )
if ( ELASTICSEARCH_USER or ELASTICSEARCH_PASSWORD )
{
return $url->request( $timeout )->login( \IPS\ELASTICSEARCH_USER, \IPS\ELASTICSEARCH_PASSWORD );
return $url->request( $timeout )->login( ELASTICSEARCH_USER, ELASTICSEARCH_PASSWORD );
}
return $url->request( $timeout );
@@ -1205,10 +1245,10 @@ class _Index extends \IPS\Content\Search\Index
/**
* Check response to see if an error was produced
*
* @param \IPS\Http\Response $response Response object
* @return string|null
* @param Response $response Response object
* @return array|null
*/
protected function getResponseError( \IPS\Http\Response $response )
protected function getResponseError( Response $response ): array|NULL
{
/* Log any errors */
if( $response->httpResponseCode != 200 AND $content = $response->decodeJson() AND isset( $content['error'] ) )
+17 -10
View File
@@ -11,44 +11,51 @@
namespace IPS\Content\Search\Elastic;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use IPS\Content;
use IPS\Content\Comment;
use IPS\Content\Item;
use IPS\Http\Url;
use function defined;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Elasticsearch Mass Indexer
*/
class _MassIndexer extends \IPS\Content\Search\Elastic\Index
class MassIndexer extends Index
{
/**
* @brief Data to store in bulk
*/
protected $data = array();
protected ?array $data = array();
/**
* Index an item
*
* @param \IPS\Content\Searchable $object Item to add
* @param Content $object Item to add
* @return void
*/
public function index( \IPS\Content\Searchable $object )
public function index( Content $object ): void
{
if ( $indexData = $this->indexData( $object ) and $indexData['index_permissions'] )
{
if ( $object instanceof \IPS\Content\Item )
if ( $object instanceof Item )
{
$parent = $object;
}
elseif ( $object instanceof \IPS\Content\Comment )
elseif ( $object instanceof Comment )
{
$parent = $object->item();
}
$this->data[] = array(
'index' => array(
'_index' => trim( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ], '/' ),
'_index' => trim( $this->url->data[ Url::COMPONENT_PATH ], '/' ),
'_type' => '_doc',
'_id' => $this->getIndexId( $object ),
)
@@ -73,7 +80,7 @@ class _MassIndexer extends \IPS\Content\Search\Elastic\Index
}
$json = implode( "\n", $json );
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( '/_bulk' ) )->setHeaders( array( 'Content-Type' => 'application/x-ndjson' ) )->post( $json . "\n" );
Index::request( $this->url->setPath( '/_bulk' ) )->setHeaders( array( 'Content-Type' => 'application/x-ndjson' ) )->post( $json . "\n" );
}
}
}
+155 -178
View File
@@ -11,60 +11,82 @@
namespace IPS\Content\Search\Elastic;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use Exception;
use IPS\Application;
use IPS\Content;
use IPS\Content\Item;
use IPS\Content\Search\Query as SearchQuery;
use IPS\Content\Search\Results;
use IPS\DateTime;
use IPS\Db;
use IPS\Http\Url;
use IPS\IPS;
use IPS\Log;
use IPS\Member;
use IPS\Member\Club;
use IPS\Settings;
use StdClass;
use function count;
use function defined;
use function in_array;
use function intval;
use function is_array;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Elasticsearch Search Query
*/
class _Query extends \IPS\Content\Search\Query
class Query extends SearchQuery
{
/**
* @brief The server URL
*/
protected $url;
protected Url $url;
/**
* @brief Filters
*/
protected $filters = array();
protected array $filters = array();
/**
* @brief "Must Not" filter
*/
protected $mustNot = array();
protected array $mustNot = array();
/**
* @brief The sort clause
*/
protected $sort = NULL;
protected array|string|null $sort = NULL;
/**
* @brief The offset
*/
protected $offset = 0;
protected int $offset = 0;
/**
* @brief index_hidden statuses
*/
protected $hiddenStatuses = NULL;
protected int|array|null $hiddenStatuses = NULL;
/**
* @brief Item classes included
*/
protected $itemClasses = NULL;
protected array|null $itemClasses = NULL;
/**
* Constructor
*
* @param \IPS\Member $member The member performing the search
* @param \IPS\Http\Url $url The server URL
* @param Member $member The member performing the search
* @param Url $url The server URL
* @return void
*/
public function __construct( \IPS\Member $member, \IPS\Http\Url $url )
public function __construct( Member $member, Url $url )
{
parent::__construct( $member );
$this->url = $url;
@@ -73,16 +95,16 @@ class _Query extends \IPS\Content\Search\Query
/**
* @var bool Stores whether or not any content type needs to be limited by the latest comment
*/
protected $lastCommentMustBeTrue = false;
protected bool $lastCommentMustBeTrue = false;
/**
* Filter by multiple content types
*
* @param array $contentFilters Array of \IPS\Content\Search\ContentFilter objects
* @param bool $type TRUE means only include results matching the filters, FALSE means exclude all results matching the filters
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
public function filterByContent( array $contentFilters, $type = TRUE )
public function filterByContent( array $contentFilters, bool $type = TRUE ): static
{
$contentFilterConditions = array();
if ( $type )
@@ -212,14 +234,14 @@ class _Query extends \IPS\Content\Search\Query
}
/* Put it together */
if( \count( $conditions ) )
if( count( $conditions ) )
{
$contentFilterConditions[] = Index::convertConditionsToQuery( $conditions );
}
}
/* Put them together */
if ( \count( $contentFilterConditions ) > 1 )
if ( count( $contentFilterConditions ) > 1 )
{
$this->filters[] = array(
'bool' => array(
@@ -229,14 +251,14 @@ class _Query extends \IPS\Content\Search\Query
}
elseif ( $type )
{
if( \count( $contentFilterConditions ) )
if( count( $contentFilterConditions ) )
{
$this->filters[] = $contentFilterConditions[0];
}
}
else
{
if( \count( $contentFilterConditions ) )
if( count( $contentFilterConditions ) )
{
$this->mustNot[] = $contentFilterConditions[0];
}
@@ -248,18 +270,18 @@ class _Query extends \IPS\Content\Search\Query
/**
* Filter by author
*
* @param \IPS\Member|int|array $author The author, or an array of author IDs
* @return \IPS\Content\Search\Query (for daisy chaining)
* @param Member|int|array $author The author, or an array of author IDs
* @return Query (for daisy chaining)
*/
public function filterByAuthor( $author )
public function filterByAuthor( Member|int|array $author ): static
{
if ( \is_array( $author ) )
if ( is_array( $author ) )
{
$this->filters[] = array( 'terms' => array( 'index_author' => $author ) );
}
else
{
$this->filters[] = array( 'term' => array( 'index_author' => $author instanceof \IPS\Member ? $author->member_id : $author ) );
$this->filters[] = array( 'term' => array( 'index_author' => $author instanceof Member ? $author->member_id : $author ) );
}
return $this;
@@ -268,10 +290,10 @@ class _Query extends \IPS\Content\Search\Query
/**
* Filter by club
*
* @param \IPS\Member\Club|int|array $club The club, or array of club IDs
* @return \IPS\Content\Search\Query (for daisy chaining)
* @param Club|int|array|null $club The club, or array of club IDs
* @return Query (for daisy chaining)
*/
public function filterByClub( $club )
public function filterByClub( Club|int|array|null $club ): static
{
if ( $club === NULL )
{
@@ -279,7 +301,7 @@ class _Query extends \IPS\Content\Search\Query
'exists' => array( 'field' => 'index_club_id' )
);
}
elseif ( \is_array( $club ) )
elseif ( is_array( $club ) )
{
$this->filters[] = array(
'terms' => array( 'index_club_id' => $club )
@@ -288,28 +310,12 @@ class _Query extends \IPS\Content\Search\Query
else
{
$this->filters[] = array(
'term' => array( 'index_club_id' => $club instanceof \IPS\Member\Club ? $club->id : $club )
'term' => array( 'index_club_id' => $club instanceof Club ? $club->id : $club )
);
}
/* Get the list of valid classes */
foreach ( \IPS\Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
{
foreach ( $object->classes as $class )
{
if ( \in_array( 'IPS\Content\Item', class_parents( $class ) ) )
{
$classesChecked[] = $class;
}
}
}
/* Give content item classes a chance to inspect and manipulate filters */
$filters = array();
foreach( $classesChecked as $itemClass )
{
$itemClass::searchEngineFiltering( $filters, $this );
}
$this->customFiltering( TRUE );
return $this;
}
@@ -317,10 +323,10 @@ class _Query extends \IPS\Content\Search\Query
/**
* Filter for profile
*
* @param \IPS\Member $member The member whose profile is being viewed
* @return \IPS\Content\Search\Query (for daisy chaining)
* @param Member $member The member whose profile is being viewed
* @return Query (for daisy chaining)
*/
public function filterForProfile( \IPS\Member $member )
public function filterForProfile( Member $member ): static
{
/* Filter by content they've posted or posts on their wall */
$this->filters[] = array(
@@ -328,71 +334,40 @@ class _Query extends \IPS\Content\Search\Query
'should' => array(
array(
'term' => array( 'index_author' => $member->member_id )
),
array(
'bool' => array(
'filter' => array(
array(
'term' => array(
'index_class' => 'IPS\core\Statuses\Status'
)
),
array(
'term' => array(
'index_container_id' => $member->member_id
)
)
)
)
)
)
)
);
/* Get the list of valid classes */
foreach ( \IPS\Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
{
foreach ( $object->classes as $class )
{
if ( \in_array( 'IPS\Content\Item', class_parents( $class ) ) )
{
$classesChecked[] = $class;
}
}
}
/* Give content item classes a chance to inspect and manipulate filters */
$filters = array();
foreach( $classesChecked as $itemClass )
{
$itemClass::searchEngineFiltering( $filters, $this );
}
$this->customFiltering( TRUE );
/* Return for daisy chaining */
/* Return for daisy-chaining */
return $this;
}
/**
* Stores the "more like this" Content object for search()
* @param null
* @param array|null $moreLikeThis
*/
protected $moreLikeThis = NULL;
protected array|null $moreLikeThis = NULL;
/**
* Filter by more like this
* @param \IPS\Content\Searchable $object
* @param Content $object
* @return Query
*/
public function filterByMoreLikeThis( \IPS\Content\Searchable $object )
public function filterByMoreLikeThis( Content $object ): static
{
$index = new \IPS\Content\Search\Elastic\Index( $this->url );
$this->moreLikeThis = $index->getIndexId( ( $object instanceof \IPS\Content\Item and $object::$firstCommentRequired ) ? $object->firstComment() : $object );
$index = new Index( $this->url );
$this->moreLikeThis = $index->getIndexId( ( $object instanceof Item and $object::$firstCommentRequired ) ? $object->firstComment() : $object );
/* Some container types cannot be cached */
$classes = [];
$conditions = [];
$noSimilarContentClasses = [];
foreach ( \IPS\Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
foreach ( Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
{
$classes = array_merge( $object->classes, $classes );
@@ -428,7 +403,7 @@ class _Query extends \IPS\Content\Search\Query
}
}
if ( \count( $blockIds ) )
if ( count( $blockIds ) )
{
$conditions[] = array(
'bool' => array(
@@ -444,7 +419,7 @@ class _Query extends \IPS\Content\Search\Query
);
}
if ( \count( $conditions ) )
if ( count( $conditions ) )
{
foreach( $conditions as $condition )
{
@@ -455,7 +430,7 @@ class _Query extends \IPS\Content\Search\Query
}
/* Prevent some things from being in similar content widget */
if ( \count( $noSimilarContentClasses ) )
if ( count( $noSimilarContentClasses ) )
{
$this->mustNot[] = array(
'bool' => array(
@@ -470,6 +445,8 @@ class _Query extends \IPS\Content\Search\Query
/* Only show non-hidden and approved items */
$this->setHiddenFilter( static::HIDDEN_VISIBLE );
return $this;
}
/**
@@ -478,9 +455,9 @@ class _Query extends \IPS\Content\Search\Query
* @param array $classes Container classes to exclude from results.
* @param array $exclude Content classes to exclude from the filter. For cases where multiple content classes may have the same container class
* such as Gallery images, comments and reviews.
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
public function filterByContainerClasses( $classes=array(), $exclude=array() )
public function filterByContainerClasses( array $classes=array(), array $exclude=array() ): static
{
if( empty( $exclude ) )
{
@@ -519,10 +496,10 @@ class _Query extends \IPS\Content\Search\Query
/**
* Filter by item author
*
* @param \IPS\Member $author The author
* @return \IPS\Content\Search\Query (for daisy chaining)
* @param Member $author The author
* @return Query (for daisy chaining)
*/
public function filterByItemAuthor( \IPS\Member $author )
public function filterByItemAuthor( Member $author ): static
{
$this->filters[] = array(
'term' => array( 'index_item_author' => $author->member_id )
@@ -536,9 +513,9 @@ class _Query extends \IPS\Content\Search\Query
* @param bool $includeContainers Include content in containers the user follows?
* @param bool $includeItems Include items and comments/reviews on items the user follows?
* @param bool $includeMembers Include content posted by members the user follows?
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
public function filterByFollowed( $includeContainers, $includeItems, $includeMembers )
public function filterByFollowed( bool $includeContainers, bool $includeItems, bool $includeMembers ): static
{
$conditions = array();
$followApps = $followAreas = $case = $containerCase = array();
@@ -556,7 +533,7 @@ class _Query extends \IPS\Content\Search\Query
else
{
$classes = array();
foreach ( \IPS\Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
foreach ( Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
{
$classes = array_merge( $object->classes, $classes );
}
@@ -565,7 +542,7 @@ class _Query extends \IPS\Content\Search\Query
/* Loop them */
foreach ( $classes as $class )
{
if( is_subclass_of( $class, 'IPS\Content\Followable' ) )
if( IPS::classUsesTrait( $class, 'IPS\Content\Followable' ) )
{
$followApps[ $class::$application ] = $class::$application;
$followArea = mb_strtolower( mb_substr( $class, mb_strrpos( $class, '\\' ) + 1 ) );
@@ -607,13 +584,13 @@ class _Query extends \IPS\Content\Search\Query
}
/* Get the stuff we follow */
foreach( \IPS\Db::i()->select( '*', 'core_follow', array( 'follow_member_id=? AND ' . \IPS\Db::i()->in( 'follow_app', $followApps ) . ' AND ' . \IPS\Db::i()->in( 'follow_area', $followAreas ), $this->member->member_id ) ) as $follow )
foreach( Db::i()->select( '*', 'core_follow', array( 'follow_member_id=? AND ' . Db::i()->in( 'follow_app', $followApps ) . ' AND ' . Db::i()->in( 'follow_area', $followAreas ), $this->member->member_id ) ) as $follow )
{
if( array_key_exists( $follow['follow_area'], $case ) )
{
$followedItems[ $follow['follow_area'] ][] = $follow['follow_rel_id'];
}
else if( \in_array( $follow['follow_area'], $containerCase ) )
else if( in_array( $follow['follow_area'], $containerCase ) )
{
$followedContainers[ $follow['follow_area'] ][] = $follow['follow_rel_id'];
}
@@ -663,7 +640,7 @@ class _Query extends \IPS\Content\Search\Query
}
/* Are we including content posted by followed members? */
if ( $includeMembers and $followed = iterator_to_array( \IPS\Db::i()->select( 'follow_rel_id', 'core_follow', array( 'follow_app=? AND follow_area=? AND follow_member_id=?', 'core', 'member', $this->member->member_id ), 'follow_rel_id asc' ) ) )
if ( $includeMembers and $followed = iterator_to_array( Db::i()->select( 'follow_rel_id', 'core_follow', array( 'follow_app=? AND follow_area=? AND follow_member_id=?', 'core', 'member', $this->member->member_id ), 'follow_rel_id asc' ) ) )
{
$conditions[] = array(
'terms' => array( 'index_author' => $followed )
@@ -671,13 +648,13 @@ class _Query extends \IPS\Content\Search\Query
}
/* Put it all together */
if ( \count( $conditions ) )
if ( count( $conditions ) )
{
$this->filters[] = array( 'bool' => array( 'should' => $conditions ) );
}
else
{
$this->filters[] = array( 'match_none' => new \StdClass );
$this->filters[] = array( 'match_none' => new StdClass );
}
/* And return */
@@ -687,9 +664,9 @@ class _Query extends \IPS\Content\Search\Query
/**
* Filter by content the user has posted in
*
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
public function filterByItemsIPostedIn()
public function filterByItemsIPostedIn(): static
{
$this->filters[] = array(
'term' => array(
@@ -703,9 +680,9 @@ class _Query extends \IPS\Content\Search\Query
* Filter by content the user has not read
*
* @note If applicable, it is more efficient to call filterByContent() before calling this method
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
public function filterByUnread()
public function filterByUnread(): static
{
/* Work out what classes we need to examine */
if ( $this->itemClasses !== NULL )
@@ -715,7 +692,7 @@ class _Query extends \IPS\Content\Search\Query
else
{
$classes = array();
foreach ( \IPS\Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
foreach ( Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
{
$classes = array_merge( $object->classes, $classes );
}
@@ -726,9 +703,9 @@ class _Query extends \IPS\Content\Search\Query
$resetTimes = $this->member->markersResetTimes( NULL );
foreach ( $classes as $class )
{
if( is_subclass_of( $class, 'IPS\Content\ReadMarkers' ) )
if( IPS::classUsesTrait( $class, 'IPS\Content\ReadMarkers' ) )
{
$containerClass = ( $class::$containerNodeClass ) ? $class::$containerNodeClass : NULL;
$containerClass = $class::$containerNodeClass ?? NULL;
$classConditions = array();
/* Work out what classes this applies to - need to specify comment and review classes */
@@ -779,21 +756,21 @@ class _Query extends \IPS\Content\Search\Query
);
/* And get the times each individual item was read for later */
$items = $this->member->markersItems( $application, \IPS\Content\Item::makeMarkerKey( $containerId ) );
if ( \count( $items ) )
$items = $this->member->markersItems( $application, $class::makeMarkerKey( $containerId ) );
if ( count( $items ) )
{
foreach( $items as $mid => $mtime )
{
if ( $mtime > $timestamp )
{
/* If an item has been moved from one container to another, the user may have a marker
in it's old location, with the previously 'read' time. In this circumstance, we need
in its old location, with the previously 'read' time. In this circumstance, we need
to only use more recent read time, otherwise the topic may be incorrectly included
in the results */
if ( \in_array( $mid, $markers ) )
if ( in_array( $mid, $markers ) )
{
$_key = array_search( $mid, $markers );
$_mtime = \intval( mb_substr( $_key, 0, mb_strpos( $_key, '.' ) ) );
$_mtime = intval( mb_substr( $_key, 0, mb_strpos( $_key, '.' ) ) );
if ( $_mtime < $mtime )
{
unset( $markers[ $_key ] );
@@ -838,7 +815,7 @@ class _Query extends \IPS\Content\Search\Query
}
$notIn = array();
if ( \count( $markers ) )
if ( count( $markers ) )
{
$useIds = array_flip( $markers );
@@ -847,7 +824,7 @@ class _Query extends \IPS\Content\Search\Query
{
if ( isset( $class::$databaseColumnMap[ $k ] ) )
{
if ( \is_array( $class::$databaseColumnMap[ $k ] ) )
if ( is_array( $class::$databaseColumnMap[ $k ] ) )
{
foreach ( $class::$databaseColumnMap[ $k ] as $v )
{
@@ -860,16 +837,16 @@ class _Query extends \IPS\Content\Search\Query
}
}
}
$dateColumnExpression = \count( $dateColumns ) > 1 ? ( 'GREATEST(' . implode( ',', $dateColumns ) . ')' ) : array_pop( $dateColumns );
$dateColumnExpression = count( $dateColumns ) > 1 ? ( 'GREATEST(' . implode( ',', $dateColumns ) . ')' ) : array_pop( $dateColumns );
foreach( \IPS\Db::i()->select( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnId. ' as _id, ' . $dateColumnExpression . ' as _date', $class::$databaseTable, \IPS\Db::i()->in( $class::$databasePrefix . $class::$databaseColumnId, array_keys( $useIds ) ) ) as $row )
foreach( Db::i()->select( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnId. ' as _id, ' . $dateColumnExpression . ' as _date', $class::$databaseTable, Db::i()->in( $class::$databasePrefix . $class::$databaseColumnId, array_keys( $useIds ) ) ) as $row )
{
if ( isset( $useIds[ $row['_id'] ] ) )
{
if ( $useIds[ $row['_id'] ] >= $row['_date'] )
{
/* Still read */
$notIn[] = \intval( $row['_id'] );
$notIn[] = intval( $row['_id'] );
}
}
}
@@ -881,7 +858,7 @@ class _Query extends \IPS\Content\Search\Query
'filter' => $classConditions
)
);
if ( \count( $notIn ) )
if ( count( $notIn ) )
{
$_condition['bool']['must_not'] = array(
'terms' => array(
@@ -894,7 +871,7 @@ class _Query extends \IPS\Content\Search\Query
}
/* Put it all together */
if ( \count( $conditions ) )
if ( count( $conditions ) )
{
$this->filters[] = array(
'bool' => array(
@@ -909,9 +886,9 @@ class _Query extends \IPS\Content\Search\Query
/**
* Filter by solved
*
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
public function filterBySolved()
public function filterBySolved(): static
{
$this->filters[] = array(
'term' => array( 'index_item_solved' => 1 )
@@ -923,9 +900,9 @@ class _Query extends \IPS\Content\Search\Query
/**
* Filter by Unsolved
*
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
public function filterByUnsolved()
public function filterByUnsolved(): static
{
$this->filters[] = array(
'term' => array( 'index_item_solved' => 0 )
@@ -937,11 +914,11 @@ class _Query extends \IPS\Content\Search\Query
/**
* Filter by start date
*
* @param \IPS\DateTime|NULL $start The start date (only results AFTER this date will be returned)
* @param \IPS\DateTime|NULL $end The end date (only results BEFORE this date will be returned)
* @return \IPS\Content\Search\Query (for daisy chaining)
* @param DateTime|NULL $start The start date (only results AFTER this date will be returned)
* @param DateTime|NULL $end The end date (only results BEFORE this date will be returned)
* @return Query (for daisy chaining)
*/
public function filterByCreateDate( \IPS\DateTime $start = NULL, \IPS\DateTime $end = NULL )
public function filterByCreateDate( DateTime|null $start = null, DateTime|null $end = null ): static
{
$range = array();
@@ -967,11 +944,11 @@ class _Query extends \IPS\Content\Search\Query
/**
* Filter by last updated date
*
* @param \IPS\DateTime|NULL $start The start date (only results AFTER this date will be returned)
* @param \IPS\DateTime|NULL $end The end date (only results BEFORE this date will be returned)
* @return \IPS\Content\Search\Query (for daisy chaining)
* @param DateTime|NULL $start The start date (only results AFTER this date will be returned)
* @param DateTime|NULL $end The end date (only results BEFORE this date will be returned)
* @return Query (for daisy chaining)
*/
public function filterByLastUpdatedDate( \IPS\DateTime $start = NULL, \IPS\DateTime $end = NULL )
public function filterByLastUpdatedDate( DateTime|null $start = null, DateTime|null $end = null ): static
{
$range = array();
@@ -998,9 +975,9 @@ class _Query extends \IPS\Content\Search\Query
* Set hidden status
*
* @param int|array|NULL $statuses The statuses (see HIDDEN_ constants) or NULL for any
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
public function setHiddenFilter( $statuses )
public function setHiddenFilter( int|array|null $statuses ): static
{
$this->hiddenStatuses = $statuses;
return $this;
@@ -1010,9 +987,9 @@ class _Query extends \IPS\Content\Search\Query
* Set page
*
* @param int $page The page number
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
public function setPage( $page )
public function setPage( int $page ): static
{
$this->offset = ( $page - 1 ) * $this->resultsToGet;
@@ -1023,9 +1000,9 @@ class _Query extends \IPS\Content\Search\Query
* Set order
*
* @param int $order Order (see ORDER_ constants)
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
public function setOrder( $order )
public function setOrder( int $order ): static
{
switch ( $order )
{
@@ -1062,16 +1039,16 @@ class _Query extends \IPS\Content\Search\Query
/**
* Debug by itemId, used for debugging purposes only
* @note No permission checks run, do not use in production
* @param $itemId
* @return void
* @param int $itemId
* @return array
*/
public function debugByItemId( $itemId )
public function debugByItemId( int $itemId ): array
{
$array = array(
'query' => [
'term' => [
'index_item_id' => [
'value' => \intval( $itemId )
'value' => $itemId
]
]
],
@@ -1082,7 +1059,7 @@ class _Query extends \IPS\Content\Search\Query
$json = json_encode( $array, JSON_PARTIAL_OUTPUT_ON_ERROR );
return \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( $json )->decodeJson();
return Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( $json )->decodeJson();
}
/**
@@ -1092,25 +1069,25 @@ class _Query extends \IPS\Content\Search\Query
* @param array|null $tags The tags to search for
* @param int $method See \IPS\Content\Search\Query::TERM_* contants
* @param string|null $operator If $term contains more than one word, determines if searching for both ("and") or any ("or") of those terms. NULL will go to admin-defined setting
* @return \IPS\Content\Search\Results
* @return Results
*/
public function search( $term = NULL, $tags = NULL, $method = 1, $operator = NULL )
public function search( string|null $term = null, array|null $tags = null, int $method = 1, string|null $operator = null ): Results
{
/* If we're looking for more results than we can fetch, we don't need to ask Elastic Search */
if( ( $this->offset + $this->resultsToGet ) > \IPS\Settings::i()->search_index_maxresults )
/* If we're looking for more results than we can fetch, we don't need to ask ElasticSearch */
if( ( $this->offset + $this->resultsToGet ) > Settings::i()->search_index_maxresults )
{
return new \IPS\Content\Search\Results( array(), 0 );
return new Results( array(), 0 );
}
$operator = $operator ?: \IPS\Settings::i()->search_default_operator;
$operator = $operator ?: Settings::i()->search_default_operator;
$must = array();
$filters = $this->filters;
/* Set our conditions for this search */
if ( $term !== NULL or ( \IPS\Settings::i()->tags_enabled AND $tags !== NULL ) )
if ( $term !== NULL or ( Settings::i()->tags_enabled AND $tags !== NULL ) )
{
$searchConditions = array();
$titleField = \IPS\Settings::i()->search_title_boost ? ( 'index_title^' . \intval( \IPS\Settings::i()->search_title_boost ) ) : 'index_title';
$titleField = Settings::i()->search_title_boost ? ( 'index_title^' . intval( Settings::i()->search_title_boost ) ) : 'index_title';
/* Build the condition for the search term */
if ( $term !== NULL )
@@ -1141,7 +1118,7 @@ class _Query extends \IPS\Content\Search\Query
'bool' => array(
'should' => array(
array(
'wildcard' => array( 'index_title' => ( \IPS\Settings::i()->search_title_boost ? array( 'value' => $term, 'boost' => \intval( \IPS\Settings::i()->search_title_boost ) ) : array( 'value' => $term ) ) )
'wildcard' => array( 'index_title' => ( Settings::i()->search_title_boost ? array( 'value' => $term, 'boost' => intval( Settings::i()->search_title_boost ) ) : array( 'value' => $term ) ) )
),
array(
'wildcard' => array( 'index_content' => array( 'value' => $term ) )
@@ -1165,7 +1142,7 @@ class _Query extends \IPS\Content\Search\Query
}
}
/* Build the condition for the tags */
if ( \IPS\Settings::i()->tags_enabled and $tags !== NULL )
if ( Settings::i()->tags_enabled and $tags !== NULL )
{
$searchConditions[] = array(
'bool' => array(
@@ -1209,7 +1186,7 @@ class _Query extends \IPS\Content\Search\Query
$filters[] = array( 'terms' => array( 'index_permissions' => array_merge( $this->permissionArray(), array( '*' ) ) ) );
if ( $this->hiddenStatuses !== NULL )
{
if ( \is_array( $this->hiddenStatuses ) )
if ( is_array( $this->hiddenStatuses ) )
{
$filters[] = array( 'terms' => array( 'index_hidden' => $this->hiddenStatuses ) );
}
@@ -1219,14 +1196,14 @@ class _Query extends \IPS\Content\Search\Query
}
}
$searchUrl = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_search' );
$searchUrl = $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_search' );
if ( $this->moreLikeThis )
{
$must[] = [
'more_like_this' => [
'fields' => [ 'index_title' ],
'like' => [ '_index' => \IPS\Settings::i()->search_elastic_index, '_id' => $this->moreLikeThis ],
'like' => [ '_index' => Settings::i()->search_elastic_index, '_id' => $this->moreLikeThis ],
'min_term_freq' => 1,
'max_query_terms' => 12,
'min_word_length' => 3,
@@ -1250,15 +1227,15 @@ class _Query extends \IPS\Content\Search\Query
);
/* Add the time decay */
if ( \IPS\Settings::i()->search_decay_factor and !$this->sort and ! $this->sort )
if ( Settings::i()->search_decay_factor and !$this->sort )
{
$query = array(
'function_score' => array(
'query' => $query,
'linear' => array(
'index_date_updated' => array(
'scale' => \intval( \IPS\Settings::i()->search_decay_days ) . 'd',
'decay' => number_format( \IPS\Settings::i()->search_decay_factor, 1, '.', '' ),
'scale' => intval( Settings::i()->search_decay_days ) . 'd',
'decay' => number_format( Settings::i()->search_decay_factor, 1, '.', '' ),
'origin' => time(),
)
)
@@ -1267,7 +1244,7 @@ class _Query extends \IPS\Content\Search\Query
}
/* Add the self boost */
if ( \IPS\Settings::i()->search_elastic_self_boost and $this->member->member_id and !$this->sort and ! $this->moreLikeThis )
if ( Settings::i()->search_elastic_self_boost and $this->member->member_id and !$this->sort and ! $this->moreLikeThis )
{
$query = array(
'function_score' => array(
@@ -1277,8 +1254,8 @@ class _Query extends \IPS\Content\Search\Query
'source' => "doc['index_author'].value == params.param_memberId ? ( _score * Float.parseFloat( params.param_booster ) ) : _score",
'lang' => 'painless',
'params' => array(
'param_memberId' => \intval( $this->member->member_id ),
'param_booster' => number_format( \IPS\Settings::i()->search_elastic_self_boost, 1, '.', '' )
'param_memberId' => intval( $this->member->member_id ),
'param_booster' => number_format( Settings::i()->search_elastic_self_boost, 1, '.', '' )
)
)
)
@@ -1299,30 +1276,30 @@ class _Query extends \IPS\Content\Search\Query
$json = json_encode( $array, JSON_PARTIAL_OUTPUT_ON_ERROR );
if ( $json === FALSE )
{
return new \IPS\Content\Search\Results( array(), 0 );
return new Results( array(), 0 );
}
/* Make the call! */
$return = \IPS\Content\Search\Elastic\Index::request( $searchUrl )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( $json )->decodeJson();
$return = Index::request( $searchUrl )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( $json )->decodeJson();
if ( isset( $return['error'] ) )
{
\IPS\Log::log( print_r( array_merge( $array, ['error' => $return['error'] ] ), TRUE ), 'elasticsearch' );
return new \IPS\Content\Search\Results( array(), 0 );
Log::log( print_r( array_merge( $array, ['error' => $return['error'] ] ), TRUE ), 'elasticsearch' );
return new Results( array(), 0 );
}
/* Set results */
$total = $return['hits']['total']['value'] ?? $return['hits']['total'];
return new \IPS\Content\Search\Results( array_map( function( $hit ) {
return new Results( array_map( function( $hit ) {
$indexData = $hit['_source'];
$indexData['index_permissions'] = implode( ',', $indexData['index_permissions'] );
$indexData['index_tags'] = $indexData['index_tags'] ? implode( ',', $indexData['index_tags'] ) : NULL;
return $indexData;
}, $return['hits']['hits'] ), $total <= \IPS\Settings::i()->search_index_maxresults ? $total : (int) \IPS\Settings::i()->search_index_maxresults );
}, $return['hits']['hits'] ), $total <= Settings::i()->search_index_maxresults ? $total : (int) Settings::i()->search_index_maxresults );
}
catch ( \Exception $e )
catch ( Exception $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
return new \IPS\Content\Search\Results( array(), 0 );
Log::log( $e, 'elasticsearch' );
return new Results( array(), 0 );
}
}
}