Version 4.3.0
This commit is contained in:
1 parent
96997ddd8e
commit
fe1acf984a
1349 files changed
+116159
-67711
No files matched your search
@@ -139,6 +139,10 @@ class _Query extends \IPS\Content\Search\Query
|
||||
{
|
||||
$clause[] = \IPS\Db::i()->in( 'index_item_id', $filter->itemIds, $filter->itemIdFilter === FALSE );
|
||||
}
|
||||
if ( $filter->objectIdFilter !== NULL )
|
||||
{
|
||||
$clause[] = \IPS\Db::i()->in( 'index_object_id', $filter->objectIds, $filter->objectIdFilter === FALSE );
|
||||
}
|
||||
|
||||
/* Minimum comments/reviews/views? */
|
||||
if ( $filter->minimumComments or $filter->minimumReviews or $filter->minimumViews )
|
||||
@@ -263,12 +267,33 @@ class _Query extends \IPS\Content\Search\Query
|
||||
$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;
|
||||
$filterResult = $this;
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->filterByAuthor( $member );
|
||||
$filterResult = $this->filterByAuthor( $member );
|
||||
}
|
||||
|
||||
/* 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, $filterResult );
|
||||
}
|
||||
|
||||
return $filterResult;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -296,7 +321,7 @@ class _Query extends \IPS\Content\Search\Query
|
||||
{
|
||||
if( empty( $exclude ) )
|
||||
{
|
||||
$this->where[] = \IPS\Db::i()->in( 'index_container_class', $classes, TRUE );
|
||||
$this->where[] = '( index_container_class IS NULL OR ' . \IPS\Db::i()->in( 'index_container_class', $classes, TRUE ) . ')';
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -309,7 +334,7 @@ class _Query extends \IPS\Content\Search\Query
|
||||
$exclude[$i] = "'" . \IPS\DB::i()->real_escape_string( $class ) . "'";
|
||||
}
|
||||
|
||||
$this->where[] = '( index_container_class NOT IN(' . implode( ',', $classes ) . ') OR index_class IN(' . implode( ',', $exclude ) . ') )';
|
||||
$this->where[] = '( index_container_class IS NULL OR index_container_class NOT IN(' . implode( ',', $classes ) . ') OR index_class IN(' . implode( ',', $exclude ) . ') )';
|
||||
}
|
||||
|
||||
return $this;
|
||||
@@ -767,7 +792,7 @@ class _Query extends \IPS\Content\Search\Query
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function setOrder( $order )
|
||||
{
|
||||
{
|
||||
switch ( $order )
|
||||
{
|
||||
case static::ORDER_NEWEST_UPDATED:
|
||||
@@ -785,6 +810,10 @@ class _Query extends \IPS\Content\Search\Query
|
||||
case static::ORDER_OLDEST_CREATED:
|
||||
$this->order = 'index_date_created ASC';
|
||||
break;
|
||||
|
||||
case static::ORDER_NEWEST_COMMENTED:
|
||||
$this->order = 'index_date_commented DESC';
|
||||
break;
|
||||
|
||||
case static::ORDER_RELEVANCY:
|
||||
$this->order = 'calcscore DESC';
|
||||
@@ -797,12 +826,13 @@ class _Query extends \IPS\Content\Search\Query
|
||||
/**
|
||||
* Build where
|
||||
*
|
||||
* @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
|
||||
* @param string|null $term The term to search for
|
||||
* @param array|null $tags The tags to search for
|
||||
* @param int $method See \IPS\Content\Search\Query::TERM_* contants
|
||||
* @param string $operator If $term contains more than one word, determines if searching for both ("and") or any ("or") of those terms
|
||||
* @return array
|
||||
*/
|
||||
protected function _searchWhereClause( $term = NULL, $tags = NULL, $method = 1 )
|
||||
protected function _searchWhereClause( $term = NULL, $tags = NULL, $method = 1, $operator = 'and' )
|
||||
{
|
||||
$tagWhere = NULL;
|
||||
$termWhere = NULL;
|
||||
@@ -850,7 +880,7 @@ class _Query extends \IPS\Content\Search\Query
|
||||
/* Do we have a term? */
|
||||
if ( $term !== NULL )
|
||||
{
|
||||
$termWhere = static::matchClause( $method & static::TERM_TITLES_ONLY ? 'index_title' : 'index_content,index_title', $term, $method & static::TERM_OR_MODE ? '' : '+' );
|
||||
$termWhere = static::matchClause( $method & static::TERM_TITLES_ONLY ? 'index_title' : 'index_content,index_title', $term, $operator === static::OPERATOR_AND ? '+' : '' );
|
||||
}
|
||||
|
||||
/* Put those two together */
|
||||
@@ -1072,7 +1102,7 @@ class _Query extends \IPS\Content\Search\Query
|
||||
{
|
||||
$extra = $columns . ' LIKE \'%' . \IPS\Db::i()->escape_string( $term ) . '%\'';
|
||||
}
|
||||
|
||||
|
||||
return array( "MATCH({$columns}) AGAINST (? IN BOOLEAN MODE) AND (" . $extra . ")", $booleanTerm );
|
||||
}
|
||||
else
|
||||
@@ -1082,6 +1112,7 @@ class _Query extends \IPS\Content\Search\Query
|
||||
}
|
||||
else
|
||||
{
|
||||
$likeWhere = array();
|
||||
foreach ( static::termAsWordsArray( $term ) as $word )
|
||||
{
|
||||
/* Add the default operator */
|
||||
@@ -1102,9 +1133,16 @@ class _Query extends \IPS\Content\Search\Query
|
||||
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 ) )
|
||||
if ( preg_match( '/^.+[\+\-~<>\.]/', $word ) )
|
||||
{
|
||||
$word = "\"" . str_replace( '"', '', ltrim( $word, $defaultOperator ) ) . "\"";
|
||||
$trimmedWord = \IPS\Db::i()->escape_string( str_replace( '"', '', ltrim( $word, $defaultOperator ) ) );
|
||||
$likes = array();
|
||||
foreach( explode( ',', $columns ) AS $column )
|
||||
{
|
||||
$likes[] = $column . ' LIKE \'%' . $trimmedWord . '%\'';
|
||||
}
|
||||
$likeWhere[] = '( ' . implode( ' or ', $likes ) . ' )';
|
||||
continue;
|
||||
}
|
||||
/* Otherwise carry on... */
|
||||
else
|
||||
@@ -1114,75 +1152,107 @@ class _Query extends \IPS\Content\Search\Query
|
||||
|
||||
/* 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 ) );
|
||||
$word = str_replace( array( '(', ')' ), '', str_replace( '@', ' ', $word ) );
|
||||
|
||||
preg_match( '#^(\+|\-|\*)#', $word, $matches );
|
||||
if ( mb_stripos( $word, "'" ) and isset( $matches[1] ) )
|
||||
{
|
||||
/* Due to MySQL bug (https://bugs.mysql.com/bug.php?id=69932) apostrophes confuse things */
|
||||
preg_match( '#^(\+|\-|\*)#', $word, $matches );
|
||||
|
||||
$word = $matches[1] . '(' . str_replace( $matches[1], '', $word ) . ')';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Add it */
|
||||
$words[] = $word;
|
||||
}
|
||||
$term = implode( ' ', $words );
|
||||
|
||||
/* Return */
|
||||
$return = array();
|
||||
|
||||
if ( $prepared )
|
||||
{
|
||||
if ( count( $words ) > 1 or mb_strstr( $term, '*' ) )
|
||||
/* Force newest as of 4.2.6 to test search results */
|
||||
if ( count( $likeWhere ) and count( $words ) )
|
||||
{
|
||||
return array( "MATCH({$columns}) AGAINST (? IN BOOLEAN MODE)", $term );
|
||||
$return = array( implode( ' AND ', $likeWhere ) . " AND MATCH({$columns}) AGAINST (? IN BOOLEAN MODE)", $term );
|
||||
}
|
||||
else if ( count( $likeWhere ) )
|
||||
{
|
||||
$return = array( implode( ' AND ', $likeWhere ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
return array( "MATCH({$columns}) AGAINST (?)", ltrim( $term, $defaultOperator ) );
|
||||
$return = array( "MATCH({$columns}) AGAINST (? IN BOOLEAN MODE)", $term );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( count( $words ) > 1 or mb_strstr( $term, '*' ) )
|
||||
/* Force newest as of 4.2.6 to test search results */
|
||||
if ( count( $likeWhere ) )
|
||||
{
|
||||
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 ) ) . "')";
|
||||
$return[] = implode( ' AND ', $likeWhere );
|
||||
}
|
||||
|
||||
$return[] = "MATCH({$columns}) AGAINST ('" . \IPS\Db::i()->escape_string( $term ) . "' IN BOOLEAN MODE)";
|
||||
|
||||
$return = implode( ' AND ', $return );
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search
|
||||
*
|
||||
* @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
|
||||
* @param string|null $term The term to search for
|
||||
* @param array|null $tags The tags to search for
|
||||
* @param int $method See \IPS\Content\Search\Query::TERM_* contants - controls where to search
|
||||
* @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
|
||||
*/
|
||||
public function search( $term = NULL, $tags = NULL, $method = 1 )
|
||||
public function search( $term = NULL, $tags = NULL, $method = 1, $operator = NULL )
|
||||
{
|
||||
/* What's our operator? */
|
||||
$operator = $operator ?: \IPS\Settings::i()->search_default_operator;
|
||||
|
||||
/* Set the select clause */
|
||||
$select = implode( ', ', $this->select );
|
||||
|
||||
/* Get the where clause */
|
||||
$where = array_merge( $this->where, $this->_searchWhereClause( $term, $tags, $method ) );
|
||||
$where = array_merge( $this->where, $this->_searchWhereClause( $term, $tags, $method, $operator ) );
|
||||
|
||||
/* 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 )
|
||||
{
|
||||
if ( \IPS\Settings::i()->search_title_boost )
|
||||
{
|
||||
$titleField = '(' . static::matchClause( 'index_title', $term, $operator === static::OPERATOR_AND ? '+' : '', FALSE ) . '*' . intval( \IPS\Settings::i()->search_title_boost ) . ')'; // The title score times multiplier
|
||||
}
|
||||
else
|
||||
{
|
||||
$titleField = '(' . static::matchClause( 'index_title', $term, $operator === static::OPERATOR_AND ? '+' : '', FALSE ) . ')';
|
||||
}
|
||||
|
||||
$select .= ', '
|
||||
. '('
|
||||
. '(' . static::matchClause( 'index_title', $term, $method & static::TERM_OR_MODE ? '' : '+', FALSE ) . '*5)' // The title score times 5
|
||||
. $titleField
|
||||
. '+'
|
||||
. '(' . static::matchClause( 'index_content,index_title', $term, $method & static::TERM_OR_MODE ? '' : '+', FALSE ) . ')' // Plus the content score times 1
|
||||
. '(' . static::matchClause( 'index_content,index_title', $term, $operator === static::OPERATOR_AND ? '+' : '', 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
|
||||
. '( ( 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';
|
||||
}
|
||||
@@ -1205,25 +1275,26 @@ class _Query extends \IPS\Content\Search\Query
|
||||
{
|
||||
$query->forceIndex( $this->forceIndex );
|
||||
}
|
||||
|
||||
|
||||
/* Return */
|
||||
$count = $this->count( $term, $tags, $method, FALSE );
|
||||
$count = $this->count( $term, $tags, $method, $operator, FALSE );
|
||||
return new \IPS\Content\Search\Results( iterator_to_array( $query ), $count );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count
|
||||
*
|
||||
* @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
|
||||
* @param string|null $term The term to search for
|
||||
* @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
|
||||
* @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, $returnCountAsInt=TRUE )
|
||||
public function count( $term = NULL, $tags = NULL, $method = 1, $operator = NULL, $returnCountAsInt=TRUE )
|
||||
{
|
||||
/* Get the where clause */
|
||||
$where = array_merge( $this->where, $this->_searchWhereClause( $term, $tags, $method ) );
|
||||
$where = array_merge( $this->where, $this->_searchWhereClause( $term, $tags, $method, $operator ) );
|
||||
|
||||
/* Construct the query */
|
||||
$query = \IPS\Db::i()->select( 'COUNT(*)', array( 'core_search_index', 'main' ), $where );
|
||||
@@ -1263,7 +1334,7 @@ class _Query extends \IPS\Content\Search\Query
|
||||
return 'newest';
|
||||
}
|
||||
|
||||
return 'relevancy';
|
||||
return parent::getDefaultSortMethod();
|
||||
}
|
||||
|
||||
protected static $tableCount = NULL;
|
||||
@@ -1282,4 +1353,77 @@ class _Query extends \IPS\Content\Search\Query
|
||||
|
||||
return static::$tableCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the term into an array of words
|
||||
*
|
||||
* @param string $term The term to search for
|
||||
* @param boolean $ignorePhrase When true, phrases are stripped of quotes and treated as normal words
|
||||
* @param int|NULL $minLength The minimum length a sequence of characters has to be before it is considered a word. If null, ft_min_word_len/innodb_ft_min_token_size is used.
|
||||
* @param int|NULL $maxLength The maximum length a sequence of characters can be for it to be considered a word. If null, ft_max_word_len/innodb_ft_max_token_size is used.
|
||||
* @return array
|
||||
*/
|
||||
public static function termAsWordsArray( $term, $ignorePhrase=FALSE, $minLength=NULL, $maxLength=NULL )
|
||||
{
|
||||
/* If we haven't set a preferred min/max length, use the MySQL configuration */
|
||||
if ( $minLength === NULL or $maxLength === NULL )
|
||||
{
|
||||
/* If we don't already know what they are, get the values from the MySQL configuration */
|
||||
if ( ( $minLength === NULL and !isset( \IPS\Data\Store::i()->mysqlMinWord ) ) or ( $maxLength === NULL and !isset( \IPS\Data\Store::i()->mysqlMaxWord ) ) )
|
||||
{
|
||||
/* The variable we need depends on whether the table is MyISAM or InnoDB */
|
||||
$tableDefinition = \IPS\Db::i()->getTableDefinition('core_search_index');
|
||||
if ( $tableDefinition['engine'] == 'InnoDB' )
|
||||
{
|
||||
$minVariable = 'innodb_ft_min_token_size';
|
||||
$maxVariable = 'innodb_ft_max_token_size';
|
||||
}
|
||||
else
|
||||
{
|
||||
$minVariable = 'ft_min_word_len';
|
||||
$maxVariable = 'ft_max_word_len';
|
||||
}
|
||||
|
||||
/* Now fetch those */
|
||||
try
|
||||
{
|
||||
foreach ( new \IPS\Db\Select( 'SHOW VARIABLES WHERE Variable_Name=? OR Variable_Name=?', array( $minVariable, $maxVariable ), \IPS\Db::i() ) as $row )
|
||||
{
|
||||
if ( $row['Variable_name'] === $minVariable )
|
||||
{
|
||||
\IPS\Data\Store::i()->mysqlMinWord = intval( $row['Value'] );
|
||||
}
|
||||
elseif ( $row['Variable_name'] === $maxVariable )
|
||||
{
|
||||
\IPS\Data\Store::i()->mysqlMaxWord = intval( $row['Value'] );
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( \IPS\Db\Exception $e ) { }
|
||||
|
||||
/* If we weren't able to get them, set sensible defaults */
|
||||
if ( !isset( \IPS\Data\Store::i()->mysqlMinWord ) )
|
||||
{
|
||||
\IPS\Data\Store::i()->mysqlMinWord = 3;
|
||||
}
|
||||
if ( !isset( \IPS\Data\Store::i()->mysqlMaxWord ) )
|
||||
{
|
||||
\IPS\Data\Store::i()->mysqlMaxWord = 84;
|
||||
}
|
||||
}
|
||||
|
||||
/* Set */
|
||||
if ( $minLength === NULL )
|
||||
{
|
||||
$minLength = \IPS\Data\Store::i()->mysqlMinWord;
|
||||
}
|
||||
if ( $maxLength === NULL )
|
||||
{
|
||||
$maxLength = \IPS\Data\Store::i()->mysqlMaxWord;
|
||||
}
|
||||
}
|
||||
|
||||
/* And then pass up */
|
||||
return parent::termAsWordsArray( $term, $ignorePhrase, $minLength, $maxLength );
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user