Version 4.5.0

This commit is contained in:
Neo committed 2025-12-19 05:51:58 -08:00
1 parent 1a0c7fd3c2
commit f79dcf067a
3791 files changed
+248032 -76372

No files matched your search

+93 -34
View File
@@ -11,7 +11,7 @@
namespace IPS\Content\Search\Elastic;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -174,8 +174,8 @@ class _Query extends \IPS\Content\Search\Query
}
}
/* Minimum views / comments / reviews */
foreach ( array( 'minimumViews' => 'index_views', 'minimumComments' => 'index_comments', 'minimumReviews' => 'index_reviews' ) as $filterKey => $indexKey )
/* Minimum comments / reviews */
foreach ( array( 'minimumComments' => 'index_comments', 'minimumReviews' => 'index_reviews' ) as $filterKey => $indexKey )
{
if ( $filter->$filterKey )
{
@@ -204,11 +204,14 @@ class _Query extends \IPS\Content\Search\Query
}
/* Put it together */
$contentFilterConditions[] = Index::convertConditionsToQuery( $conditions );
if( \count( $conditions ) )
{
$contentFilterConditions[] = Index::convertConditionsToQuery( $conditions );
}
}
/* Put them together */
if ( count( $contentFilterConditions ) > 1 )
if ( \count( $contentFilterConditions ) > 1 )
{
$this->filters[] = array(
'bool' => array(
@@ -218,11 +221,17 @@ class _Query extends \IPS\Content\Search\Query
}
elseif ( $type )
{
$this->filters[] = $contentFilterConditions[0];
if( \count( $contentFilterConditions ) )
{
$this->filters[] = $contentFilterConditions[0];
}
}
else
{
$this->mustNot[] = $contentFilterConditions[0];
if( \count( $contentFilterConditions ) )
{
$this->mustNot[] = $contentFilterConditions[0];
}
}
return $this;
@@ -236,7 +245,7 @@ class _Query extends \IPS\Content\Search\Query
*/
public function filterByAuthor( $author )
{
if ( is_array( $author ) )
if ( \is_array( $author ) )
{
$this->filters[] = array( 'terms' => array( 'index_author' => $author ) );
}
@@ -262,7 +271,7 @@ class _Query extends \IPS\Content\Search\Query
'exists' => array( 'field' => 'index_club_id' )
);
}
elseif ( is_array( $club ) )
elseif ( \is_array( $club ) )
{
$this->filters[] = array(
'terms' => array( 'index_club_id' => $club )
@@ -274,6 +283,25 @@ class _Query extends \IPS\Content\Search\Query
'term' => array( 'index_club_id' => $club instanceof \IPS\Member\Club ? $club->id : $club )
);
}
/* Get the list of valid classes */
foreach ( \IPS\Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
{
foreach ( $object->classes as $class )
{
if ( \in_array( 'IPS\Content\Item', class_parents( $class ) ) )
{
$classesChecked[] = $class;
}
}
}
/* Give content item classes a chance to inspect and manipulate filters */
$filters = array();
foreach( $classesChecked as $itemClass )
{
$itemClass::searchEngineFiltering( $filters, $this );
}
return $this;
}
@@ -318,7 +346,7 @@ class _Query extends \IPS\Content\Search\Query
{
foreach ( $object->classes as $class )
{
if ( in_array( 'IPS\Content\Item', class_parents( $class ) ) )
if ( \in_array( 'IPS\Content\Item', class_parents( $class ) ) )
{
$classesChecked[] = $class;
}
@@ -399,7 +427,7 @@ class _Query extends \IPS\Content\Search\Query
*
* @param bool $includeContainers Include content in containers the user follows?
* @param bool $includeItems Include items and comments/reviews on items the user follows?
* @param bool $includeContainers Include content posted by members the user follows?
* @param bool $includeMembers Include content posted by members the user follows?
* @return \IPS\Content\Search\Query (for daisy chaining)
*/
public function filterByFollowed( $includeContainers, $includeItems, $includeMembers )
@@ -477,7 +505,7 @@ class _Query extends \IPS\Content\Search\Query
{
$followedItems[ $follow['follow_area'] ][] = $follow['follow_rel_id'];
}
else if( in_array( $follow['follow_area'], $containerCase ) )
else if( \in_array( $follow['follow_area'], $containerCase ) )
{
$followedContainers[ $follow['follow_area'] ][] = $follow['follow_rel_id'];
}
@@ -537,7 +565,7 @@ class _Query extends \IPS\Content\Search\Query
}
/* Put it all together */
if ( count( $conditions ) )
if ( \count( $conditions ) )
{
$this->filters[] = array( 'bool' => array( 'should' => $conditions ) );
}
@@ -646,7 +674,7 @@ class _Query extends \IPS\Content\Search\Query
/* And get the times each individual item was read for later */
$items = $this->member->markersItems( $application, \IPS\Content\Item::makeMarkerKey( $containerId ) );
if ( count( $items ) )
if ( \count( $items ) )
{
foreach( $items as $mid => $mtime )
{
@@ -656,10 +684,10 @@ class _Query extends \IPS\Content\Search\Query
in it's old location, with the previously 'read' time. In this circumstance, we need
to only use more recent read time, otherwise the topic may be incorrectly included
in the results */
if ( in_array( $mid, $markers ) )
if ( \in_array( $mid, $markers ) )
{
$_key = array_search( $mid, $markers );
$_mtime = intval( mb_substr( $_key, 0, mb_strpos( $_key, '.' ) ) );
$_mtime = \intval( mb_substr( $_key, 0, mb_strpos( $_key, '.' ) ) );
if ( $_mtime < $mtime )
{
unset( $markers[ $_key ] );
@@ -704,7 +732,7 @@ class _Query extends \IPS\Content\Search\Query
}
$notIn = array();
if ( count( $markers ) )
if ( \count( $markers ) )
{
$useIds = array_flip( $markers );
@@ -713,7 +741,7 @@ class _Query extends \IPS\Content\Search\Query
{
if ( isset( $class::$databaseColumnMap[ $k ] ) )
{
if ( is_array( $class::$databaseColumnMap[ $k ] ) )
if ( \is_array( $class::$databaseColumnMap[ $k ] ) )
{
foreach ( $class::$databaseColumnMap[ $k ] as $v )
{
@@ -726,7 +754,7 @@ class _Query extends \IPS\Content\Search\Query
}
}
}
$dateColumnExpression = count( $dateColumns ) > 1 ? ( 'GREATEST(' . implode( ',', $dateColumns ) . ')' ) : array_pop( $dateColumns );
$dateColumnExpression = \count( $dateColumns ) > 1 ? ( 'GREATEST(' . implode( ',', $dateColumns ) . ')' ) : array_pop( $dateColumns );
foreach( \IPS\Db::i()->select( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnId. ' as _id, ' . $dateColumnExpression . ' as _date', $class::$databaseTable, \IPS\Db::i()->in( $class::$databasePrefix . $class::$databaseColumnId, array_keys( $useIds ) ) ) as $row )
{
@@ -735,7 +763,7 @@ class _Query extends \IPS\Content\Search\Query
if ( $useIds[ $row['_id'] ] >= $row['_date'] )
{
/* Still read */
$notIn[] = intval( $row['_id'] );
$notIn[] = \intval( $row['_id'] );
}
}
}
@@ -747,7 +775,7 @@ class _Query extends \IPS\Content\Search\Query
'filter' => $classConditions
)
);
if ( count( $notIn ) )
if ( \count( $notIn ) )
{
$_condition['bool']['must_not'] = array(
'terms' => array(
@@ -760,7 +788,7 @@ class _Query extends \IPS\Content\Search\Query
}
/* Put it all together */
if ( count( $conditions ) )
if ( \count( $conditions ) )
{
$this->filters[] = array(
'bool' => array(
@@ -907,7 +935,7 @@ class _Query extends \IPS\Content\Search\Query
public function search( $term = NULL, $tags = NULL, $method = 1, $operator = NULL )
{
/* If we're looking for more results than we can fetch, we don't need to ask Elastic Search */
if( ( $this->offset + $this->resultsToGet ) >= \IPS\Settings::i()->search_index_maxresults )
if( ( $this->offset + $this->resultsToGet ) > \IPS\Settings::i()->search_index_maxresults )
{
return new \IPS\Content\Search\Results( array(), 0 );
}
@@ -920,11 +948,12 @@ class _Query extends \IPS\Content\Search\Query
if ( $term !== NULL or $tags !== NULL )
{
$searchConditions = array();
$titleField = \IPS\Settings::i()->search_title_boost ? ( 'index_title^' . intval( \IPS\Settings::i()->search_title_boost ) ) : 'index_title';
$titleField = \IPS\Settings::i()->search_title_boost ? ( 'index_title^' . \intval( \IPS\Settings::i()->search_title_boost ) ) : 'index_title';
/* Build the condition for the search term */
if ( $term !== NULL )
{
/* If term is in "quotes" handle it as a phrase */
if ( static::termIsPhrase( $term ) )
{
$term = trim( $term, '"' );
@@ -937,6 +966,30 @@ class _Query extends \IPS\Content\Search\Query
$searchConditions[] = array( 'multi_match' => array( 'query' => $term, 'fields' => array( 'index_content', $titleField ), 'type' => 'phrase' ) );
}
}
/* If term contains * wildcard, but doesn't start with it (from elasticsearch docs: "In order to prevent extremely slow wildcard queries, a wildcard term should not start with one of the wildcards * or ?"), handle it as a wildcard search - NOTE this will not use the analyzer */
elseif ( preg_match( '/^[^\s\*].*\*.*$/', $term ) )
{
if ( $method & static::TERM_TITLES_ONLY )
{
$searchConditions[] = array( 'wildcard' => array( 'index_title' => array( 'value' => $term ) ) );
}
else
{
$searchConditions[] = array(
'bool' => array(
'should' => array(
array(
'wildcard' => array( 'index_title' => ( \IPS\Settings::i()->search_title_boost ? array( 'value' => $term, 'boost' => \intval( \IPS\Settings::i()->search_title_boost ) ) : array( 'value' => $term ) ) )
),
array(
'wildcard' => array( 'index_content' => array( 'value' => $term ) )
)
)
)
);
}
}
/* Otherwise just do it as a match search */
else
{
if ( $method & static::TERM_TITLES_ONLY )
@@ -988,7 +1041,7 @@ class _Query extends \IPS\Content\Search\Query
$filters[] = array( 'terms' => array( 'index_permissions' => array_merge( $this->permissionArray(), array( '*' ) ) ) );
if ( $this->hiddenStatuses !== NULL )
{
if ( is_array( $this->hiddenStatuses ) )
if ( \is_array( $this->hiddenStatuses ) )
{
$filters[] = array( 'terms' => array( 'index_hidden' => $this->hiddenStatuses ) );
}
@@ -1018,8 +1071,9 @@ class _Query extends \IPS\Content\Search\Query
'query' => $query,
'linear' => array(
'index_date_updated' => array(
'scale' => intval( \IPS\Settings::i()->search_decay_days ) . 'd',
'decay' => number_format( \IPS\Settings::i()->search_decay_factor, 1, '.', '' )
'scale' => \intval( \IPS\Settings::i()->search_decay_days ) . 'd',
'decay' => number_format( \IPS\Settings::i()->search_decay_factor, 1, '.', '' ),
'origin' => time(),
)
)
)
@@ -1034,8 +1088,12 @@ class _Query extends \IPS\Content\Search\Query
'query' => $query,
'script_score' => array(
'script' => array(
'inline' => "doc['index_author'].value == " . intval( $this->member->member_id ) . " ? ( _score * " . number_format( \IPS\Settings::i()->search_elastic_self_boost, 1, '.', '' ) . " ) : _score",
'lang' => 'painless'
'source' => "doc['index_author'].value == params.param_memberId ? ( _score * Float.parseFloat( params.param_booster ) ) : _score",
'lang' => 'painless',
'params' => array(
'param_memberId' => \intval( $this->member->member_id ),
'param_booster' => number_format( \IPS\Settings::i()->search_elastic_self_boost, 1, '.', '' )
)
)
)
)
@@ -1057,7 +1115,7 @@ class _Query extends \IPS\Content\Search\Query
{
return new \IPS\Content\Search\Results( array(), 0 );
}
/* Make the call! */
$return = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_search' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( $json )->decodeJson();
if ( isset( $return['error'] ) )
@@ -1065,14 +1123,15 @@ class _Query extends \IPS\Content\Search\Query
\IPS\Log::log( print_r( $return['error'], TRUE ), 'elasticsearch' );
return new \IPS\Content\Search\Results( array(), 0 );
}
/* Set results */
/* Set results */
$total = $return['hits']['total']['value'] ?? $return['hits']['total'];
return new \IPS\Content\Search\Results( array_map( function( $hit ) {
$indexData = $hit['_source'];
$indexData['index_permissions'] = implode( ',', $indexData['index_permissions'] );
$indexData['index_tags'] = $indexData['index_tags'] ? implode( ',', $indexData['index_tags'] ) : NULL;
return $indexData;
}, $return['hits']['hits'] ), $return['hits']['total'] <= \IPS\Settings::i()->search_index_maxresults ? $return['hits']['total'] : (int) \IPS\Settings::i()->search_index_maxresults );
}, $return['hits']['hits'] ), $total <= \IPS\Settings::i()->search_index_maxresults ? $total : (int) \IPS\Settings::i()->search_index_maxresults );
}
catch ( \Exception $e )
{