Version 4.1.19

This commit is contained in:
Neo committed 2025-12-19 05:38:56 -08:00
1 parent 28bd025b35
commit 2bd5025018
2674 files changed
+233518 -77766

No files matched your search

+264 -48
View File
@@ -2,9 +2,9 @@
/**
* @brief MySQL Search Index
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Social Suite
* @package IPS Community Suite
* @since 21 Aug 2014
* @version SVN_VERSION_NUMBER
*/
@@ -33,10 +33,14 @@ class _Index extends \IPS\Content\Search\Index
{
/* Get the index data */
$indexData = $this->indexData( $object );
$class = get_class( $object );
/* If we got the data... */
if( $indexData )
{
/* MySQL specific look up hash for easy referencing across tables */
$indexData['index_class_type_id_hash'] = md5( $indexData['index_class'] . ':' . $indexData['index_object_id'] );
/* If nobody has permission to access it, just remove it */
if ( !$indexData['index_permissions'] )
{
@@ -45,52 +49,159 @@ class _Index extends \IPS\Content\Search\Index
/* Otherwise, go ahead... */
else
{
/* Index it */
$newIndexId = \IPS\Db::i()->replace( 'core_search_index', $indexData, TRUE );
/* If that was successful... */
if ( $newIndexId )
$existingData = NULL;
$existingIndexId = NULL;
$resetLastComment = FALSE;
$newIndexId = NULL;
try
{
$class = get_class( $object );
$databaseColumnId = $object::$databaseColumnId;
$existingData = \IPS\Db::i()->select( 'index_id, index_class, index_object_id, index_item_id, index_hidden, index_is_last_comment, index_author', 'core_search_index', array( 'index_class=? AND index_object_id=?', $indexData['index_class'], $indexData['index_object_id'] ) )->first();
$existingIndexId = $existingData['index_id'];
}
catch( \Exception $e ) { }
/* Adjust tags */
$tags = NULL;
if ( array_key_exists( 'index_tags', $indexData ) )
{
$tags = array_filter( array_merge ( array( $indexData['index_prefix'] ), explode( ',', $indexData['index_tags'] ) ) );
$prefix = $indexData['index_prefix'];
unset( $indexData['index_tags'] );
unset( $indexData['index_prefix'] );
}
if ( $object instanceof \IPS\Content\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'] = 0;
/* Set index_item_index_id on other index items */
if ( $object instanceof \IPS\Content\Item )
$resetLastComment = TRUE;
}
else if ( $indexData['index_is_last_comment'] and $indexData['index_item_id'] )
{
$classes = array( $class );
/* If this is the latest comment, unflag what was set before on both item and comment */
if ( $object instanceof \IPS\Content\Comment )
{
$itemClass = $object::$itemClass;
if ( ! $itemClass::$firstCommentRequired )
{
$classes[] = $itemClass;
}
if ( isset( $itemClass::$reviewClass ) )
{
$classes[] = $itemClass::$reviewClass;
}
}
else if ( $object instanceof \IPS\Content\Item )
{
$subClasses = array( $class );
if ( isset( $class::$commentClass ) )
{
$subClasses[] = $class::$commentClass;
$classes[] = $class::$commentClass;
}
if ( isset( $class::$reviewClass ) )
{
$subClasses[] = $class::$reviewClass;
}
\IPS\Db::i()->update( 'core_search_index', array( 'index_item_index_id' => $newIndexId ), array( array( \IPS\Db::i()->in( 'index_class', $subClasses ) ), array( 'index_item_id=?', $object->$databaseColumnId ) ) );
}
elseif ( $object instanceof \IPS\Content\Comment )
{
$itemClass = $object::$itemClass;
if ( $itemClass::$firstCommentRequired and $object->isFirst() )
{
$itemColumnId = $class::$databaseColumnMap['item'];
\IPS\Db::i()->update( 'core_search_index', array( 'index_item_index_id' => $newIndexId ), array( \IPS\Db::i()->in( 'index_class', array( $class, $class::$itemClass ) ) . ' AND index_item_id=?', $object->$itemColumnId ) );
$classes[] = $class::$reviewClass;
}
}
/* And also set core_follow.follow_index_id */
if ( $object instanceof \IPS\Content\Comment )
{
$class = $class::$itemClass;
}
\IPS\Db::i()->update( 'core_follow', array( 'follow_index_id' => $newIndexId ), array( 'follow_app=? AND follow_area=? AND follow_rel_id=?', $object::$application, mb_strtolower( mb_substr( $class, mb_strrpos( $class, '\\' ) + 1 ) ), $object->$databaseColumnId ) );
\IPS\Db::i()->update( 'core_search_index', array( 'index_is_last_comment' => 0 ), array( \IPS\Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=?', $indexData['index_item_id'] ) );
}
/* If this is the latest comment, unflag what was set before */
if ( $indexData['index_is_last_comment'] and $indexData['index_item_id'] )
if ( $existingData !== NULL and ( $indexData['index_class'] == $existingData['index_class'] and $indexData['index_object_id'] == $existingData['index_object_id'] ) )
{
\IPS\Db::i()->update( 'core_search_index', $indexData, array( 'index_class=? and index_object_id=?', $indexData['index_class'], $indexData['index_object_id'] ) );
$newIndexId = $existingIndexId;
}
else
{
if ( $existingData !== NULL )
{
\IPS\Db::i()->update( 'core_search_index', array( 'index_is_last_comment' => 0 ), array( 'index_class=? AND index_item_id=? AND index_id<>?', $indexData['index_class'], $indexData['index_item_id'], $newIndexId ) );
\IPS\Db::i()->delete( 'core_search_index', array( 'index_class=? and index_object_id=?', $indexData['index_class'], $indexData['index_object_id'] ) );
}
try
{
$newIndexId = \IPS\Db::i()->insert( 'core_search_index', $indexData );
}
catch( \IPS\Db\Exception $e )
{
if ( $e->getCode() == 1062 )
{
/* Duplicate key which could be caused by a race condition on rebuild. Use replace in this case, as it is more expensive than an insert, so we only use it when we have to */
$newIndexId = \IPS\Db::i()->replace( 'core_search_index', $indexData );
}
}
}
/* If that was successful... */
if ( $newIndexId )
{
/* Remove existing tags */
if ( $existingIndexId )
{
\IPS\Db::i()->delete( 'core_search_index_tags', array( 'index_id=?', $existingIndexId ) );
}
/* Add add them back if we have any */
if ( count( $tags ) )
{
foreach( $tags as $tag )
{
\IPS\Db::i()->replace( 'core_search_index_tags', array( 'index_id' => $newIndexId, 'index_tag' => $tag, 'index_is_prefix' => ( $tag == $prefix ) ) );
}
}
/* Populate the map table, we always populate it under the item class regardless */
if ( $existingData == NULL or ( $existingData !== NULL and ( $existingData['index_author'] != $indexData['index_author'] OR $existingData['index_item_id'] != $indexData['index_item_id'] ) ) )
{
\IPS\Db::i()->replace( 'core_search_index_item_map', array( 'index_author_id' => $indexData['index_author'], 'index_item_id' => $indexData['index_item_id'], 'index_class' => ( $object instanceof \IPS\Content\Comment ? $object::$itemClass : $class ) ) );
}
$databaseColumnId = $object::$databaseColumnId;
/* Set index_item_index_id on other index items */
if ( $existingIndexId != $newIndexId )
{
if ( $object instanceof \IPS\Content\Item )
{
$subClasses = array( $class );
if ( isset( $class::$commentClass ) )
{
$subClasses[] = $class::$commentClass;
}
if ( isset( $class::$reviewClass ) )
{
$subClasses[] = $class::$reviewClass;
}
\IPS\Db::i()->update( 'core_search_index', array( 'index_item_index_id' => $newIndexId ), array( array( \IPS\Db::i()->in( 'index_class', $subClasses ) ), array( 'index_item_id=?', $object->$databaseColumnId ) ) );
}
elseif ( $object instanceof \IPS\Content\Comment )
{
$itemClass = $object::$itemClass;
if ( $itemClass::$firstCommentRequired and $object->isFirst() )
{
$itemColumnId = $class::$databaseColumnMap['item'];
\IPS\Db::i()->update( 'core_search_index', array( 'index_item_index_id' => $newIndexId ), array( \IPS\Db::i()->in( 'index_class', array( $class, $class::$itemClass ) ) . ' AND index_item_id=?', $object->$itemColumnId ) );
}
}
/* And also set core_follow.follow_index_id */
if ( $object instanceof \IPS\Content\Comment )
{
$class = $class::$itemClass;
}
\IPS\Db::i()->update( 'core_follow', array( 'follow_index_id' => $newIndexId ), array( 'follow_app=? AND follow_area=? AND follow_rel_id=?', $object::$application, mb_strtolower( mb_substr( $class, mb_strrpos( $class, '\\' ) + 1 ) ), $object->$databaseColumnId ) );
}
}
if ( $resetLastComment )
{
$this->resetLastComment( array( $indexData['index_class'] ), $indexData['index_item_id'] );
}
}
}
@@ -118,22 +229,93 @@ class _Index extends \IPS\Content\Search\Index
{
$class = get_class( $object );
$idColumn = $class::$databaseColumnId;
/* Tags */
$this->_deleteTagsFromIndex( $class, $object->$idColumn );
\IPS\Db::i()->delete( 'core_search_index', array( 'index_class=? AND index_object_id=?', $class, $object->$idColumn ) );
/* If this was a comment, we really need to reset the index_is_last_comment flag if it was set */
if ( $object instanceof \IPS\Content\Comment )
{
$itemClass = $object::$itemClass;
$classes = array( $class );
if ( ! $itemClass::$firstCommentRequired )
{
$classes[] = $itemClass;
}
if ( isset( $itemClass::$reviewClass ) )
{
$classes[] = $itemClass::$reviewClass;
}
try
{
$this->resetLastComment( $classes, $object->mapped('item') );
}
catch( \Exception $ex ) { }
/* We need to see if this is the only comment the author has in this item and if so, remove their map */
if ( ! \IPS\Db::i()->select( 'COUNT(*)', 'core_search_index', array( \IPS\Db::i()->in('index_class', $classes ) . ' and index_item_id=? and index_author=?', $object->mapped('item'), (int) $object->mapped('author') ) )->first() )
{
try
{
\IPS\Db::i()->delete( 'core_search_index_item_map', array( 'index_class=? AND index_item_id=? and index_author_id=?', $itemClass, $object->mapped('item'), (int) $object->mapped('author') ) );
}
catch( \Exception $ex ) { }
}
}
else if ( $object instanceof \IPS\Content\Item )
{
/* Just remove all rows matching the item and class. */
\IPS\Db::i()->delete( 'core_search_index_item_map', array( 'index_class=? AND index_item_id=?', $class, $object->$idColumn ) );
}
if ( isset( $class::$commentClass ) )
{
$commentClass = $class::$commentClass;
$this->_deleteTagsFromIndex( $commentClass, $object->$idColumn );
\IPS\Db::i()->delete( 'core_search_index', array( 'index_class=? AND index_item_id=?', $commentClass, $object->$idColumn ) );
}
if ( isset( $class::$reviewClass ) )
{
$reviewClass = $class::$reviewClass;
$this->_deleteTagsFromIndex( $reviewClass, $object->$idColumn );
\IPS\Db::i()->delete( 'core_search_index', array( 'index_class=? AND index_item_id=?', $reviewClass, $object->$idColumn ) );
}
}
/**
* Return the index IDs associated with this class and $id
*
* @param \IPS\Content object $class The class
* @param int $id The index_item_id
* @return array
*/
protected function _deleteTagsFromIndex( $class, $id )
{
try
{
$ids = iterator_to_array( \IPS\Db::i()->select( 'index_id', 'core_search_index', array( 'index_class=? AND index_item_id=?', $class, $id ) ) );
}
catch( \Exception $ex )
{
$ids = FALSE;
}
if ( is_array( $ids ) and count( $ids ) < 1000 )
{
\IPS\Db::i()->delete( 'core_search_index_tags', array( \IPS\Db::i()->in( 'index_id', $ids ) ) );
}
else
{
\IPS\Db::i()->delete( 'core_search_index_tags', array( 'index_id IN( ? )', \IPS\Db::i()->select( 'index_id', 'core_search_index', array( 'index_class=? AND index_item_id=?', $class, $id ) ) ) );
}
}
/**
* Removes all content for a classs
*
@@ -149,24 +331,27 @@ class _Index extends \IPS\Content\Search\Index
$where[] = array( 'index_container_id=?', $containerId );
}
\IPS\Db::i()->delete( 'core_search_index_item_map', array( 'index_item_id IN( ? )', \IPS\Db::i()->select( 'index_item_id', 'core_search_index', $where ) ) );
\IPS\Db::i()->delete( 'core_search_index_tags', array( 'index_id IN( ? )', \IPS\Db::i()->select( 'index_id', 'core_search_index', $where ) ) );
\IPS\Db::i()->delete( 'core_search_index', $where );
}
/**
* Mass Update (when permissions change, for example)
*
* @param string $class The class
* @param int|NULL $containerId The container ID to update, or NULL
* @param int|NULL $itemId The item ID to update, or NULL
* @param string|NULL $newPermissions New permissions (if applicable)
* @param int|NULL $newHiddenStatus New hidden status (if applicable) special value 2 can be used to indicate hidden only by parent
* @param int|NULL $newContainer New container ID (if applicable)
* @param int|NULL $authorId The author ID to update, or NULL
* @param int|NULL $newItemId The new item ID (if applicable)
* @param int|NULL $newItemAuthorId The new item author ID (if applicable)
* @param string $class The class
* @param int|NULL $containerId The container ID to update, or NULL
* @param int|NULL $itemId The item ID to update, or NULL
* @param string|NULL $newPermissions New permissions (if applicable)
* @param int|NULL $newHiddenStatus New hidden status (if applicable) special value 2 can be used to indicate hidden only by parent
* @param int|NULL $newContainer New container ID (if applicable)
* @param int|NULL $authorId The author ID to update, or NULL
* @param int|NULL $newItemId The new item ID (if applicable)
* @param int|NULL $newItemAuthorId The new item author ID (if applicable)
* @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 )
public function massUpdate( $class, $containerId = NULL, $itemId = NULL, $newPermissions = NULL, $newHiddenStatus = NULL, $newContainer = NULL, $authorId = NULL, $newItemId = NULL, $newItemAuthorId = NULL, $addAuthorToPermissions = FALSE )
{
$where = array( array( 'index_class=?', $class ) );
if ( $containerId !== NULL )
@@ -204,6 +389,12 @@ class _Index extends \IPS\Content\Search\Index
{
\IPS\Db::i()->update( 'core_search_index', $update, $where );
}
if ( $addAuthorToPermissions )
{
$addAuthorToPermissionsWhere = $where;
$addAuthorToPermissionsWhere[] = array( 'index_author<>0' );
\IPS\Db::i()->update( 'core_search_index', "index_permissions = CONCAT( index_permissions, ',m.', index_author )", $addAuthorToPermissionsWhere );
}
if ( $newHiddenStatus !== NULL )
{
@@ -239,7 +430,6 @@ class _Index extends \IPS\Content\Search\Index
if ( $item::$firstCommentRequired )
{
$update['index_title'] = NULL;
$update['index_tags'] = NULL;
}
\IPS\Db::i()->update( 'core_search_index', $update, array( 'index_class=? AND index_item_id=?', $item::$commentClass, $item->$idColumn ) );
@@ -264,11 +454,37 @@ class _Index extends \IPS\Content\Search\Index
{
if ( $cutoff )
{
\IPS\DB::i()->delete( 'core_search_index', array( 'index_date_updated < ?', $cutoff->getTimestamp() ) );
\IPS\Db::i()->delete( 'core_search_index_item_map', array( 'index_item_id IN( ? )', \IPS\Db::i()->select( 'index_item_id', 'core_search_index', array( 'index_date_updated < ?', $cutoff->getTimestamp() ) ) ) );
\IPS\Db::i()->delete( 'core_search_index_tags', array( 'index_id IN( ? )', \IPS\Db::i()->select( 'index_id', 'core_search_index', array( 'index_date_updated < ?', $cutoff->getTimestamp() ) ) ) );
\IPS\Db::i()->delete( 'core_search_index', array( 'index_date_updated < ?', $cutoff->getTimestamp() ) );
}
else
{
\IPS\DB::i()->delete( 'core_search_index' );
\IPS\Db::i()->delete( 'core_search_index_item_map' );
\IPS\Db::i()->delete( 'core_search_index_tags' );
\IPS\Db::i()->delete( 'core_search_index' );
}
}
/**
* 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
* @return void
*/
public function resetLastComment( $classes, $indexItemId )
{
\IPS\Db::i()->update( 'core_search_index', array( 'index_is_last_comment' => 0 ), array( \IPS\Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=?', $indexItemId ) );
try
{
$latest = \IPS\Db::i()->select( 'index_object_id, index_date_updated, index_class', 'core_search_index', array( \IPS\Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? and index_hidden=0', $indexItemId ), 'index_date_created DESC', array( 0, 1 ) )->first();
\IPS\Db::i()->update( 'core_search_index', array( 'index_is_last_comment' => 1 ), array( 'index_class=? AND index_object_id=?', $latest['index_class'], $latest['index_object_id'] ) );
/* Now reset the item index with the latest comment time */
\IPS\Db::i()->update( 'core_search_index', array( 'index_date_updated' => $latest['index_date_updated'] ), array( \IPS\Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? AND index_object_id=?', $indexItemId, $indexItemId ) );
}
catch( \Exception $ex ) { }
}
}
+575 -129
View File
@@ -2,9 +2,9 @@
/**
* @brief MySQL Search Query
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Social Suite
* @package IPS Community Suite
* @since 21 Aug 2014
* @version SVN_VERSION_NUMBER
*/
@@ -38,6 +38,11 @@ class _Query extends \IPS\Content\Search\Query
*/
protected $hiddenClause = NULL;
/**
* @brief The WHERE clause for last updated date
*/
protected $lastUpdatedClause = NULL;
/**
* @brief The offset
*/
@@ -57,6 +62,17 @@ class _Query extends \IPS\Content\Search\Query
* @brief Item classes included
*/
protected $itemClasses = NULL;
/**
* @brief Force specific table index
*/
protected $forceIndex = NULL;
/**
* @brief Filter by items I posted in?
* @see filterByItemsIPostedIn()
*/
protected $filterByItemsIPostedIn = FALSE;
/**
* Filter by multiple content types
@@ -74,7 +90,7 @@ class _Query extends \IPS\Content\Search\Query
{
$this->itemClasses = array();
}
/* Loop the filters */
foreach ( $contentFilters as $filter )
{
@@ -101,6 +117,18 @@ class _Query extends \IPS\Content\Search\Query
$clause[] = \IPS\Db::i()->in( 'index_container_id', $filter->containerIds, $filter->containerIdFilter === FALSE );
}
$itemClass = $filter->itemClass;
if ( isset( $itemClass::$containerNodeClass ) )
{
$containerClass = $itemClass::$containerNodeClass;
$unsearchableIds = $containerClass::unsearchableNodeIds();
if ( $unsearchableIds != NULL )
{
$clause[] = \IPS\Db::i()->in( 'index_container_id', $unsearchableIds, TRUE );
}
}
/* Set the item IDs */
if ( $filter->itemIdFilter !== NULL )
{
@@ -181,7 +209,7 @@ class _Query extends \IPS\Content\Search\Query
}
return $this;
}
}
/**
* Filter for profile
@@ -191,9 +219,26 @@ class _Query extends \IPS\Content\Search\Query
*/
public function filterForProfile( \IPS\Member $member )
{
$this->where[] = array( '( index_author=? OR ( index_class=? AND index_container_id=? ) )', $member->member_id, 'IPS\core\Statuses\Status', $member->member_id );
return $this;
/* Check to see if this member has even had anyone write on their status */
$count = \IPS\Db::i()->select( 'COUNT(*)', 'core_search_index', array( 'index_class=? AND index_container_id=?', 'IPS\core\Statuses\Status', $member->member_id ) )->first();
if ( $count )
{
if ( $count > 500 )
{
$this->where[] = array( '( index_author=? OR ( index_class=? AND index_container_id=? ) )', $member->member_id, 'IPS\core\Statuses\Status', $member->member_id );
}
else
{
$this->where[] = array( '( index_author=? OR (' . \IPS\Db::i()->in( 'index_id', iterator_to_array( \IPS\Db::i()->select( 'index_id', 'core_search_index', array( 'index_class=? AND index_container_id=?', 'IPS\core\Statuses\Status', $member->member_id ) ) ) ) . ') )', $member->member_id );
}
return $this;
}
else
{
return $this->filterByAuthor( $member );
}
}
/**
@@ -221,6 +266,9 @@ class _Query extends \IPS\Content\Search\Query
{
$where = array();
$params = array();
$followApps = $followAreas = $case = $containerCase = array();
$followedItems = array();
$followedContainers = array();
/* Are we including items or containers? */
if ( $includeContainers or $includeItems )
@@ -240,60 +288,114 @@ class _Query extends \IPS\Content\Search\Query
}
/* Loop them */
foreach ( $classes as $class )
{
if( is_subclass_of( $class, 'IPS\Content\Followable' ) )
{
/* Construct a subquery which will get the IDs of the items of this class that we follow */
$subQueryWhere = array( array( 'follow_app=?', $class::$application ) );
$followApps[ $class::$application ] = $class::$application;
$followArea = mb_strtolower( mb_substr( $class, mb_strrpos( $class, '\\' ) + 1 ) );
if ( $includeContainers and $includeItems )
{
$subQueryWhere[] = array( '( follow_area=? OR follow_area=? )', mb_strtolower( mb_substr( $class::$containerNodeClass, mb_strrpos( $class::$containerNodeClass, '\\' ) + 1 ) ), mb_strtolower( mb_substr( $class, mb_strrpos( $class, '\\' ) + 1 ) ) );
$followAreas[] = mb_strtolower( mb_substr( $class::$containerNodeClass, mb_strrpos( $class::$containerNodeClass, '\\' ) + 1 ) );
$followAreas[] = $followArea;
}
elseif ( $includeItems )
{
$subQueryWhere[] = array( 'follow_area=?', mb_strtolower( mb_substr( $class, mb_strrpos( $class, '\\' ) + 1 ) ) );
$followAreas[] = $followArea;
}
elseif ( $includeContainers )
{
$subQueryWhere[] = array( 'follow_area=?', mb_strtolower( mb_substr( $class::$containerNodeClass, mb_strrpos( $class::$containerNodeClass, '\\' ) + 1 ) ) );
$followAreas[] = mb_strtolower( mb_substr( $class::$containerNodeClass, mb_strrpos( $class::$containerNodeClass, '\\' ) + 1 ) );
}
$subQueryWhere[] = array( 'follow_member_id=?', $this->member->member_id );
$subQuery = \IPS\Db::i()->select( 'follow_rel_id', 'core_follow', $subQueryWhere );
/* If we're only doing one class, there's no point in doing "item_class=? AND ( item_class=? AND item_id IN(?) )" - we can just do "item_class=? AND item_id IN(?)" */
if ( count( $classes ) == 1 )
/* Work out what classes this applies to - need to specify comment and review classes */
if ( ! $class::$firstCommentRequired )
{
$where[] = 'index_item_id IN(?)';
$params[] = $subQuery;
$case[ $followArea ][] = $class;
}
/* But otherwise we need to group by class, because item IDs are not unique */
else
if( $includeContainers )
{
/* Work out what classes this applies to - need to specify comment and review classes */
$_classes = array( $class );
if ( isset( $class::$commentClass ) )
{
$_classes[] = $class::$commentClass;
}
if ( isset( $class::$reviewClass ) )
{
$_classes[] = $class::$reviewClass;
}
/* Constuct the where */
$where[] = '( ' . \IPS\Db::i()->in( 'index_class', $_classes ) . ' AND index_item_id IN( ? ) )';
$params[] = $subQuery;
$containerCase[ $followArea ] = mb_strtolower( mb_substr( $class::$containerNodeClass, mb_strrpos( $class::$containerNodeClass, '\\' ) + 1 ) ) ;
}
if ( isset( $class::$commentClass ) )
{
$case[ $followArea ][] = $class::$commentClass;
}
if ( isset( $class::$reviewClass ) )
{
$case[ $followArea ][] = $class::$reviewClass;
}
}
}
/* 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 )
{
if( array_key_exists( $follow['follow_area'], $case ) )
{
$followedItems[ $follow['follow_area'] ][] = $follow['follow_rel_id'];
}
else if( in_array( $follow['follow_area'], $containerCase ) )
{
$followedContainers[ $follow['follow_area'] ][] = $follow['follow_rel_id'];
}
}
}
foreach( $followedItems as $area => $item )
{
$where[] = '( ' . \IPS\Db::i()->in( 'index_class', $case[ $area ] ) . " AND index_item_id IN(" . implode( ',', $item ) . ") )";
}
foreach( $followedContainers as $area => $container )
{
$indexClasses = array();
foreach( $containerCase as $followArea => $containerArea )
{
if( $containerArea == $area )
{
$indexClasses = $case[ $followArea ];
}
}
$where[] = '( ' . \IPS\Db::i()->in( 'index_class', $indexClasses ) . " AND index_container_id IN(" . implode( ',', $container ) . ") )";
}
/* Are we including content posted by followed members? */
if ( $includeMembers )
{
$where[] = 'index_author IN(?)';
$params[] = \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 ) );
/* Another area where a small result set can drastically slow down the entire query */
try
{
$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', array( 0, 501 ) ) );
if ( count( $followed ) == 501 )
{
/* Assume we have loads of matches, so do a full query */
$where[] = 'index_author IN(?)';
$params[] = \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 ) );
}
else if ( count( $followed ) )
{
/* IN is not a SIN. It's been a long day */
$where[] = \IPS\Db::i()->in( 'index_author', $followed );
}
else
{
/* There are no results */
$this->where[] = "1=2 /*Filter by followed returned nothing*/";
}
}
catch( \UnderflowException $ex )
{
/* There are no results */
$this->where[] = "1=2 /*Filter by followed returned nothing*/";
}
}
/* Put it all together */
@@ -301,33 +403,30 @@ class _Query extends \IPS\Content\Search\Query
{
$this->where[] = array_merge( array( '( ' . implode( ' OR ', $where ) . ' )' ), $params );
}
else
{
/* If we want to filter by followed content, and we don't actually follow any content then we shouldn't return anything */
$this->where[] = "1=2 /*Filter by followed returned nothing*/";
}
/* And return */
return $this;
}
/**
* Filter by content the user has posted in
* Filter by content the user has posted in. This must be at the end of the chain.
*
* @return \IPS\Content\Search\Query (for daisy chaining)
*/
public function filterByItemsIPostedIn()
{
$this->joins[] = array(
'from' => \IPS\Db::i()->select(
'index_item_index_id',
array('core_search_index', 'sub' ),
array( array( 'index_author=' . intval( $this->member->member_id ) ) ),
NULL, NULL, NULL, NULL, \IPS\Db::SELECT_DISTINCT
),
'where' => 'sub.index_item_index_id=main.index_item_index_id',
'type' => 'STRAIGHT_JOIN'
);
{
/* We have to set a property because we need the other data like other filters and ordering to figure this out */
$this->filterByItemsIPostedIn = TRUE;
/* Return for daisy chaining */
return $this;
}
/**
* Filter by content the user has not read
*
@@ -382,8 +481,11 @@ class _Query extends \IPS\Content\Search\Query
}
}
/* Put it all together */
$this->where[] = array_merge( array( '( ' . implode( ' OR ', $where ) . ' )' ), $params );
if ( count( $where ) )
{
/* Put it all together */
$this->where[] = array_merge( array( '( ' . implode( ' OR ', $where ) . ' )' ), $params );
}
}
/**
@@ -442,6 +544,11 @@ class _Query extends \IPS\Content\Search\Query
{
unset( $markers[ $_key ] );
}
/* If the existing timestamp is higher, retain that since we reset the $markers array below */
else
{
$mtime = $_mtime;
}
}
$markers[ $mtime . '.' . $mid ] = $mid;
@@ -546,11 +653,11 @@ class _Query extends \IPS\Content\Search\Query
{
if ( $start )
{
$this->where[] = array( 'index_date_updated>?', $start->getTimestamp() );
$this->lastUpdatedClause[] = array( 'index_date_updated>?', $start->getTimestamp() );
}
if ( $end )
{
$this->where[] = array( 'index_date_updated<?', $end->getTimestamp() );
$this->lastUpdatedClause[] = array( 'index_date_updated<?', $end->getTimestamp() );
}
return $this;
}
@@ -615,7 +722,7 @@ class _Query extends \IPS\Content\Search\Query
break;
case static::ORDER_OLDEST_CREATED:
$this->order = 'index_date_updated DESC';
$this->order = 'index_date_created ASC';
break;
case static::ORDER_RELEVANCY:
@@ -636,86 +743,77 @@ class _Query extends \IPS\Content\Search\Query
*/
protected function _searchWhereClause( $term = NULL, $tags = NULL, $method = 1 )
{
/* Boolean tags */
$tagWhere = NULL;
$termWhere = NULL;
/* Do we have tags? */
if ( $tags !== NULL )
{
$theTags = array();
foreach( $tags as $tagEntry )
$itemsOnlyTagSearch = '';
if ( $method & static::TAGS_MATCH_ITEMS_ONLY )
{
foreach( explode( ' ', $tagEntry ) as $_term )
$itemsOnlyTagSearch = 'index_item_index_id=index_id AND ';
}
/* Large index tables and small tag tables can cause a significant slow down in this query execution, so we attempt to pre-fetch some results */
try
{
$tagIds = iterator_to_array( \IPS\Db::i()->select( 'index_id', 'core_search_index_tags', array( \IPS\Db::i()->in( 'index_tag', $tags ) ), 'index_id asc', array( 0, 501 ) ) );
/* Now, if we have 501 results, then we have to assume there are more, so the join is required */
if ( count( $tagIds ) == 501 )
{
if( mb_strlen( $_term ) > 2 )
{
$theTags[] = "+" . $_term;
}
else
{
$theTags[] = $_term;
}
$tagWhere = array( $itemsOnlyTagSearch . 'index_item_index_id IN ( ? )', \IPS\Db::i()->select( 'index_id', 'core_search_index_tags', array( \IPS\Db::i()->in( 'index_tag', $tags ) ) ) );
}
else
{
$tagWhere = array( $itemsOnlyTagSearch . \IPS\Db::i()->in( 'index_item_index_id', $tagIds ) );
}
}
catch( \UnderflowException $ex )
{
/* No matches at all */
if ( $method & static::TERM_AND_TAGS )
{
/* We want to match term and tags, so return an impossible result set */
$tagWhere = array( "1=2" );
}
else
{
$tagWhere = array( "1=1" );
}
$searchTags[] = implode( ' ', $theTags );
}
}
/* Do we have a term? */
$where = array();
if ( $term !== NULL )
{
$indexes = 'index_content,index_title';
if ( $method & static::TERM_TITLES_ONLY )
$termWhere = static::matchClause( $method & static::TERM_TITLES_ONLY ? 'index_title' : 'index_content,index_title', $term, $method & static::TERM_OR_MODE ? '' : '+' );
}
/* Put those two together */
if ( $termWhere !== NULL and $tagWhere !== NULL )
{
if ( $method & static::TERM_OR_TAGS )
{
$indexes = 'index_title';
$where[] = array_merge( array( '( ( ' . array_shift( $termWhere ) . ' ) OR ( ' . array_shift( $tagWhere ) . ' ) )' ), array_merge( $termWhere, $tagWhere ) );
}
/* Default to 'AND' boolean mode search unless asked otherwise*/
if ( ! ( $method & static::TERM_OR_MODE ) and ! static::termIsPhrase( $term ) )
{
$theWords = array();
foreach( static::termAsWordsArray( $term ) as $_term )
{
$potentialOperator = mb_substr( $_term, 0, 1 );
if ( ! in_array( $potentialOperator, array( '-', '+', '~' ) ) )
{
if( mb_strlen( $_term ) > 2 )
{
$theWords[] = "+" . $_term;
}
}
}
$term = implode( ' ', $theWords );
}
/* If we also have tags, create a combined where */
if ( $tags !== NULL )
{
if ( $method & static::TERM_OR_TAGS )
{
$where[] = array( "( MATCH({$indexes}) AGAINST (? IN BOOLEAN MODE) ) OR " . \IPS\Db::i()->findInSet( 'index_tags', $tags ), $term );
}
else
{
$where[] = array( "MATCH({$indexes}) AGAINST (? IN BOOLEAN MODE)", $term );
$where[] = \IPS\Db::i()->findInSet( 'index_tags', $tags );
}
}
/* Or just use the term */
else
{
$where[] = array( "MATCH({$indexes}) AGAINST (? IN BOOLEAN MODE)", $term );
$where[] = $termWhere;
$where[] = $tagWhere;
}
}
/* Or do we have tags? */
elseif ( $tags !== NULL )
{
$where[] = \IPS\Db::i()->findInSet( 'index_tags', $tags );
if ( $this->order = 'calcscore DESC' )
{
$this->setOrder( static::ORDER_NEWEST_UPDATED );
}
/* Or just use the term if that's all we have */
elseif ( $termWhere !== NULL )
{
$where[] = $termWhere;
}
/* Or just use tags if that's all we have */
elseif ( $tagWhere !== NULL )
{
$where[] = $tagWhere;
}
/* Only get stuff we have permission for */
@@ -725,16 +823,271 @@ class _Query extends \IPS\Content\Search\Query
$where[] = $this->hiddenClause;
}
/* MOST PERTINENT search? */
if ( mb_substr( $this->order, 0, 9 ) === 'calcscore' )
/* Filer by items I posted in? */
if ( $this->filterByItemsIPostedIn and $this->member->member_id )
{
$this->select['main'] .= ',' . "( ( MATCH (index_title) AGAINST ('" . \IPS\Db::i()->escape_string( $term ) ."' IN BOOLEAN MODE) * 5 ) + (MATCH ({$indexes}) AGAINST ('" . \IPS\Db::i()->escape_string( $term ) ."' IN BOOLEAN MODE) ) ) / POWER( ( ( UNIX_TIMESTAMP( NOW() ) - index_date_updated ) / 3600 ) + 2, 1.5 ) AS calcscore";
/* Another area where a small result set can drastically slow down the entire query */
try
{
/* Work out what classes we need to examine */
if ( $this->itemClasses !== NULL )
{
$classes = $this->itemClasses;
}
else
{
$classes = array();
foreach ( \IPS\Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
{
$classes = array_merge( $object->classes, $classes );
}
}
$results = iterator_to_array( \IPS\Db::i()->select( 'index_item_id, index_class', array( 'core_search_index_item_map', 'sub' ), array( array( 'index_author_id=' . intval( $this->member->member_id ) . ' AND ' . \IPS\Db::i()->in( 'index_class', $classes ) ) ), 'index_item_id desc', array( 0, 501 ) ) );
if ( count( $results ) == 501 )
{
/* Assume we have loads of matches, so do a full query */
$subClause = array( array( 'sub.index_item_id=main.index_item_id AND index_author_id=' . intval( $this->member->member_id ) . ' AND ' . $this->makeEmbeddedIfClauseForSearchMapTable('sub.index_class') ) );
$query = \IPS\Db::i()->select( 'index_item_id', array( 'core_search_index_item_map', 'sub' ), $subClause );
$where[] = array( 'EXISTS(?)', $query );
}
else if ( count( $results ) )
{
$classIds = array();
foreach( $results as $result )
{
$classIds[ $result['index_class'] ][] = $result['index_item_id'];
}
$subWhere = array();
foreach( $classIds as $class => $ids )
{
$stack = array( $class );
if ( isset( $class::$commentClass ) )
{
$stack[] = $class::$commentClass;
}
$subWhere[] = '(' . \IPS\Db::i()->in( 'index_class', $stack ) . ' and ' . \IPS\Db::i()->in( 'index_item_id', $ids ) . ')';
}
/* IN is not a SIN. It's been a long day */
$where[] = '(' . implode( ' OR ', $subWhere ) . ')';
}
else
{
/* There are no results */
$where[] = "1=2 /*Filter by items I posted in returned nothing*/";
}
}
catch( \UnderflowException $ex )
{
/* There are no results */
$where[] = "1=2 /*Filter by items I posted in returned nothing*/";
}
}
/* Filter by last updated? */
if ( $this->lastUpdatedClause !== NULL )
{
foreach( $this->lastUpdatedClause as $clause )
{
$where[] = $clause;
}
}
/* Return */
return $where;
}
/**
* Makes an embedded search clause to convert comment classes to item classes
*
* @param string $column Column to use in the clause
* @return string
*/
protected function makeEmbeddedIfClauseForSearchMapTable( $column )
{
/* Work out what classes we need to examine */
if ( $this->itemClasses !== NULL )
{
$classes = $this->itemClasses;
}
else
{
$classes = array();
foreach ( \IPS\Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
{
$classes = array_merge( $object->classes, $classes );
}
}
$stack = array();
foreach( $classes as $class )
{
if ( isset( $class::$commentClass ) )
{
$stack[ $class::$commentClass ] = $class;
}
}
if ( count( $stack ) )
{
$if = '';
foreach( $stack as $commentClass => $class )
{
$if .= " IF( main.index_class='" . \IPS\Db::i()->escape_string( $commentClass ) . "', '" . \IPS\Db::i()->escape_string( $class ) . "', ";
}
$if .= 'main.index_class' . str_repeat( ' )', count( $stack ) );
}
return $column . '=' . ( $if == '' ? $column : $if );
}
/**
* Match clause
*
* @param string $columns Columns to match against (e.g. "index_tags" or "index_title,index_content")
* @param string $term The term
* @param string $defaultOperator The default operator to add to each word if there isn't one - "+" is "and mode"; "" is "or mode"
* @param bool $prepared If FALSE, does not use prepared statements (used for the sorting algorithm because you can't use ?s in the select clause)
* @return array|string
*/
public static function matchClause( $columns, $term, $defaultOperator='+', $prepared=TRUE )
{
/* Loop the words */
$words = array();
/* If we have a phrase, we'd normally use the boolean engine to search it, but this is incredibly slow on larger tables (5+ million rows) because MySQL searches all the words in the word index
but if some words are below the minimum length, it performs a full table scan which is very expensive.
The solution here is to break down the phrase (i love the chocolate cake) into usable keywords ('love +chocolate +cake') and back up the full text results with a LIKE '%i love the chocolate cake%'
search which is actually very efficient as MySQL performs a search on the FT index first to narrow down the results */
if ( static::termIsPhrase( $term ) )
{
$term = str_replace( array( "'", '"' ), '', $term );
foreach ( static::termAsWordsArray( $term ) as $word )
{
/* Operators are not allowed */
$word = preg_replace( '/^([\+\-~<>]){1,}/', '', $word );
$word = preg_replace( '/([\+\-~\*<>]){2,}$/', '$1', $word );
/* +* and +- are not allowed anywhere in the word */
$word = str_replace( array( '+*', '+-', '+' ), '', $word );
/* Trailing + or -s are not allowed */
$word = rtrim( $word, '+-' );
/* Nor is @ or parenthesis (while paranthesis can be used to group words and apply operators to the group,
(e.g. "+apple +(>turnover <strudel)") - it's unlikely a user intends this behaviour) */
$word = str_replace( array( '(', ')' ), '', str_replace( '@', ' ', $word ) );
if ( $word )
{
$words[] = $word;
}
}
$booleanTerm = implode( ' +', $words );
/* Return */
if ( $prepared )
{
if ( mb_strstr( $columns, ',' ) )
{
$like = array();
$extra = '';
foreach( explode( ',', $columns ) as $col )
{
$like[] = $col . ' LIKE \'%' . \IPS\Db::i()->escape_string( $term ) . '%\'';
}
$extra = implode( ' OR ', $like );
}
else
{
$extra = $columns . ' LIKE \'%' . \IPS\Db::i()->escape_string( $term ) . '%\'';
}
return array( "MATCH({$columns}) AGAINST (? IN BOOLEAN MODE) AND (" . $extra . ")", $booleanTerm );
}
else
{
return "MATCH({$columns}) AGAINST ('" . \IPS\Db::i()->escape_string( $booleanTerm ) . "' IN BOOLEAN MODE)";
}
}
else
{
foreach ( static::termAsWordsArray( $term ) as $word )
{
/* Add the default operator */
if ( $defaultOperator and !in_array( mb_substr( $word, 0, 1 ), array( '+', '-', '~', '<', '>' ) ) )
{
/* Clear out leading * symbols so we don't end up with +*a** */
$word = $defaultOperator . ltrim( $word, '*' );
}
/* Double operators are not allowed */
$word = preg_replace( '/^([\+\-~\*<>]){2,}/', '$1', $word );
$word = preg_replace( '/([\+\-~\*<>]){2,}$/', '$1', $word );
/* Trailing + or -s are not allowed */
$word = rtrim( $word, '+-' );
/* These rules only apply if we're not in a quoted phrase... */
if ( !static::termIsPhrase( $word ) )
{
/* We can't have any other operators as MySQL will interpret them as a separate word. If they exist, wrap the word in quotes */
if ( preg_match( '/^.+[\+\-~<>]/', $word ) )
{
$word = "\"" . str_replace( '"', '', ltrim( $word, $defaultOperator ) ) . "\"";
}
/* Otherwise carry on... */
else
{
/* +* and +- are not allowed anywhere in the word */
$word = str_replace( array( '+*', '+-' ), '+', $word );
/* Nor is @ or parenthesis (while paranthesis can be used to group words and apply operators to the group,
(e.g. "+apple +(>turnover <strudel)") - it's unlikely a user intends this behaviour) */
$word = str_replace( array( '(', ')' ), '', str_replace( '@', ' ', $word ) );
}
}
/* Add it */
$words[] = $word;
}
$term = implode( ' ', $words );
/* Return */
if ( $prepared )
{
if ( count( $words ) > 1 )
{
return array( "MATCH({$columns}) AGAINST (? IN BOOLEAN MODE)", $term );
}
else
{
return array( "MATCH({$columns}) AGAINST (?)", ltrim( $term, $defaultOperator ) );
}
}
else
{
if ( count( $words ) > 1 )
{
return "MATCH({$columns}) AGAINST ('" . \IPS\Db::i()->escape_string( $term ) . "' IN BOOLEAN MODE)";
}
else
{
return "MATCH({$columns}) AGAINST ('" . \IPS\Db::i()->escape_string( ltrim( $term, $defaultOperator ) ) . "')";
}
}
}
}
/**
* Search
*
@@ -745,14 +1098,55 @@ class _Query extends \IPS\Content\Search\Query
*/
public function search( $term = NULL, $tags = NULL, $method = 1 )
{
/* Set the select clause */
$select = implode( ', ', $this->select );
/* Get the where clause */
$where = array_merge( $this->where, $this->_searchWhereClause( $term, $tags, $method ) );
$query = \IPS\Db::i()->select( implode( ', ', $this->select ), array( 'core_search_index', 'main' ), $where, $this->order, array( $this->offset, $this->resultsToGet ), NULL, NULL );
/* Set order clause */
$order = $this->order;
/* But we're sorting by relevancy, we need to actually select that value with our fancy algorithm */
if ( mb_substr( $this->order, 0, 9 ) === 'calcscore' )
{
/* But we can only do that if there's a term (rather than tag-only) */
if ( $term !== NULL )
{
$select .= ', '
. '('
. '(' . static::matchClause( 'index_title', $term, $method & static::TERM_OR_MODE ? '' : '+', FALSE ) . '*5)' // The title score times 5
. '+'
. '(' . static::matchClause( 'index_content,index_title', $term, $method & static::TERM_OR_MODE ? '' : '+', FALSE ) . ')' // Plus the content score times 1
. ')'
. '/' // Divided by
. 'POWER('
. '( ( UNIX_TIMESTAMP( NOW() ) - ( CASE WHEN index_date_updated <= UNIX_TIMESTAMP( NOW() ) THEN index_date_updated ELSE 0 END )) / 3600 ) + 2' // The number of days between now and the updated date, plus 2
. ',1.5)' // To the power of 1.5
. ' AS calcscore';
}
/* So if we don't have a term, fallback to last updated */
else
{
$order = 'index_date_updated DESC';
}
}
/* Construct the query */
$query = \IPS\Db::i()->select( $select, array( 'core_search_index', 'main' ), $where, $order, array( $this->offset, $this->resultsToGet ), NULL, NULL );
foreach ( $this->joins as $data )
{
$query->join( $data['from'], $data['where'], isset( $data['type'] ) ? $data['type'] : 'LEFT' );
}
$count = $this->count( $term, $tags, $method );
/* Force index? */
if ( $this->forceIndex )
{
$query->forceIndex( $this->forceIndex );
}
/* Return */
$count = $this->count( $term, $tags, $method, FALSE );
return new \IPS\Content\Search\Results( iterator_to_array( $query ), $count );
}
@@ -762,17 +1156,69 @@ class _Query extends \IPS\Content\Search\Query
* @param string|null $term The term to search for
* @param array|null $tags The tags to search for
* @param int $method \IPS\Content\Search\Index::i()->TERM_OR_TAGS or \IPS\Content\Search\Index::i()->TERM_AND_TAGS
* @return int
* @param boolean $returnCountAsInt If TRUE, it will return the count as an integer, when FALSE it will return the \IPS\Db\Select object
* @return \IPS\Db\Query|int
*/
public function count( $term = NULL, $tags = NULL, $method = 1 )
public function count( $term = NULL, $tags = NULL, $method = 1, $returnCountAsInt=TRUE )
{
$query = \IPS\Db::i()->select( 'COUNT(*)', array( 'core_search_index', 'main' ), array_merge( $this->where, $this->_searchWhereClause( $term, $tags, $method ) ) );
/* Get the where clause */
$where = array_merge( $this->where, $this->_searchWhereClause( $term, $tags, $method ) );
/* Construct the query */
$query = \IPS\Db::i()->select( 'COUNT(*)', array( 'core_search_index', 'main' ), $where );
foreach ( $this->joins as $data )
{
$query->join( $data['from'], $data['where'], isset( $data['type'] ) ? $data['type'] : 'LEFT' );
}
return $query->first();
/* Return */
return ( ! $returnCountAsInt ) ? $query : $query->first();
}
/**
* Get the default date cut off
*
* @return string
*/
public function getDefaultDateCutOff()
{
if ( static::getTableSize() >= \IPS\USE_MYSQL_SEARCH_BASIC_MODE_THRESHOLD )
{
return 'year';
}
return 'any';
}
/**
* Get the default sort method
*
* @return string
*/
public function getDefaultSortMethod()
{
if ( static::getTableSize() >= \IPS\USE_MYSQL_SEARCH_BASIC_MODE_THRESHOLD )
{
return 'newest';
}
return 'relevancy';
}
protected static $tableCount = NULL;
/**
* Get the row count from the core_search_index table
*
* @return string
*/
protected static function getTableSize()
{
if ( static::$tableCount === NULL )
{
static::$tableCount = \IPS\Db::i()->select( 'COUNT(*)', 'core_search_index' )->first();
}
return static::$tableCount;
}
}