Version 4.1.19
This commit is contained in:
1 parent
28bd025b35
commit
2bd5025018
2674 files changed
+233518
-77766
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user