Version 4.3.0
This commit is contained in:
1 parent
96997ddd8e
commit
fe1acf984a
1349 files changed
+116159
-67711
No files matched your search
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Search results paginated response
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 17 Oct 2017
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Search;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract Search Query
|
||||
*/
|
||||
class _ApiResponse extends \IPS\Api\PaginatedResponse
|
||||
{
|
||||
/**
|
||||
* Data to output
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getOutput()
|
||||
{
|
||||
/* Get the query object from the stored select */
|
||||
$queryObject = $this->select[0];
|
||||
|
||||
/* Set pagination */
|
||||
$queryObject->setLimit( $this->resultsPerPage );
|
||||
$queryObject->setPage( $this->page );
|
||||
|
||||
/* Run query */
|
||||
$searchResults = $queryObject->search(
|
||||
$this->select[2],
|
||||
$this->select[3],
|
||||
$this->select[1] + \IPS\Content\Search\Query::TAGS_MATCH_ITEMS_ONLY,
|
||||
$this->select[4]
|
||||
);
|
||||
|
||||
$this->count = $searchResults->count( TRUE );
|
||||
|
||||
$results = array();
|
||||
foreach ( $searchResults as $result )
|
||||
{
|
||||
$resultData = $result->asArray();
|
||||
$indexClass = $resultData['indexData']['index_class'];
|
||||
$itemClass = ( in_array( 'IPS\Content\Comment', class_parents( $indexClass ) ) ) ? $indexClass::$itemClass : $indexClass;
|
||||
|
||||
/* Container details */
|
||||
$containerUrl = NULL;
|
||||
$containerTitle = NULL;
|
||||
if ( isset( $itemClass::$containerNodeClass ) )
|
||||
{
|
||||
$containerClass = $itemClass::$containerNodeClass;
|
||||
$containerTitle = \IPS\Member::loggedIn()->language()->addToStack( $containerClass::$titleLangPrefix . $resultData['indexData']['index_container_id'], 'NULL', array( 'escape' => true ) );
|
||||
$containerUrl = $containerClass::urlFromIndexData( $resultData['indexData'], $resultData['itemData'], $resultData['containerData'] );
|
||||
}
|
||||
|
||||
/* Reputation - if we are showing the total value, then we need to load them up and total up all of the values */
|
||||
if ( \IPS\Settings::i()->reaction_count_display == 'count' )
|
||||
{
|
||||
$repCount = 0;
|
||||
foreach( $resultData['reputationData'] AS $memberId => $reactionId )
|
||||
{
|
||||
try
|
||||
{
|
||||
$repCount += \IPS\Content\Reaction::load( $reactionId )->value;
|
||||
}
|
||||
catch( \OutOfRangeException $e ) {}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$repCount = count( $resultData['reputationData'] );
|
||||
}
|
||||
|
||||
$commentCount = NULL;
|
||||
|
||||
if( isset( $itemClass::$databaseColumnMap['num_comments'] ) )
|
||||
{
|
||||
$commentCount = $resultData['itemData'][ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['num_comments'] ];
|
||||
|
||||
if( $itemClass::$firstCommentRequired === TRUE )
|
||||
{
|
||||
$commentCount -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
$reviewCount = NULL;
|
||||
|
||||
if( isset( $itemClass::$databaseColumnMap['num_reviews'] ) )
|
||||
{
|
||||
$reviewCount = $resultData['itemData'][ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['num_reviews'] ];
|
||||
}
|
||||
|
||||
$results[] = array(
|
||||
'title' => $resultData['itemData'][ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['title'] ],
|
||||
'content' => $resultData['indexData']['index_content'],
|
||||
'class' => $indexClass,
|
||||
'objectId' => $resultData['indexData']['index_object_id'],
|
||||
'itemClass' => $itemClass,
|
||||
'itemId' => $resultData['indexData']['index_item_id'],
|
||||
'started' => $result->createdDate->rfc3339(),
|
||||
'updated' => $result->lastUpdatedDate->rfc3339(),
|
||||
'itemUrl' => (string) $itemClass::urlFromIndexData( $resultData['indexData'], $resultData['itemData'] ),
|
||||
'objectUrl' => (string) $itemClass::urlFromIndexData( $resultData['indexData'], $resultData['itemData'] )->setQueryString( array( 'do' => 'findComment', 'comment' => $resultData['indexData']['index_object_id'] ) ),
|
||||
'reputation' => $repCount,
|
||||
'comments' => $commentCount,
|
||||
'reviews' => $reviewCount,
|
||||
'container' => $containerTitle,
|
||||
'containerUrl' => (string) $containerUrl,
|
||||
'author' => $resultData['authorData']['name'],
|
||||
'authorUrl' => $resultData['authorData']['member_id'] ? (string) \IPS\Http\Url::internal( "app=core&module=members&controller=profile&id={$resultData['authorData']['member_id']}", 'front', 'profile', $resultData['authorData']['members_seo_name'] ) : NULL,
|
||||
'authorPhoto' => \IPS\Member::photoUrl( $resultData['authorData'], FALSE ),
|
||||
'authorPhotoThumbnail' => \IPS\Member::photoUrl( $resultData['authorData'] ),
|
||||
'tags' => ( !empty( $resultData['indexData']['index_tags'] ) ) ? explode( ',', $resultData['indexData']['index_tags'] ) : array()
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'page' => $this->page,
|
||||
'perPage' => $this->resultsPerPage,
|
||||
'totalResults' => $this->count,
|
||||
'totalPages' => ceil( $this->count / $this->resultsPerPage ),
|
||||
'results' => $results
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -58,6 +58,16 @@ class _ContentFilter
|
||||
*/
|
||||
public $itemIds = array();
|
||||
|
||||
/**
|
||||
* @brief Object ID filter
|
||||
*/
|
||||
public $objectIdFilter = NULL;
|
||||
|
||||
/**
|
||||
* @brief Object IDs
|
||||
*/
|
||||
public $objectIds = array();
|
||||
|
||||
/**
|
||||
* @brief Minimum comments
|
||||
*/
|
||||
@@ -157,6 +167,20 @@ class _ContentFilter
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only include results with specific IDs
|
||||
*
|
||||
* @param array $ids Acceptable object IDs
|
||||
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
|
||||
*/
|
||||
public function onlyInIds( array $ids )
|
||||
{
|
||||
$this->objectIdFilter = TRUE;
|
||||
$this->objectIds = $ids;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exclude results in items
|
||||
*
|
||||
|
||||
@@ -0,0 +1,999 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Elasticsearch Search Index
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 31 Oct 2017
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Search\Elastic;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Elasticsearch Search Index
|
||||
*/
|
||||
class _Index extends \IPS\Content\Search\Index
|
||||
{
|
||||
/**
|
||||
* @brief The server URL
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param \IPS\Http\Url $url The server URL
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( \IPS\Http\Url $url )
|
||||
{
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initalize when first setting up
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
$this->url->request()->delete();
|
||||
$this->url->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( array(
|
||||
'mappings' => array(
|
||||
'content' => array(
|
||||
'properties' => array(
|
||||
'index_class' => array( 'type' => 'keyword' ),
|
||||
'index_object_id' => array( 'type' => 'long' ),
|
||||
'index_item_id' => array( 'type' => 'long' ),
|
||||
'index_container_class' => array( 'type' => 'keyword' ),
|
||||
'index_container_id' => array( 'type' => 'long' ),
|
||||
'index_title' => array(
|
||||
'type' => 'text',
|
||||
'analyzer' => \IPS\Settings::i()->search_elastic_analyzer,
|
||||
),
|
||||
'index_content' => array(
|
||||
'type' => 'text',
|
||||
'analyzer' => \IPS\Settings::i()->search_elastic_analyzer,
|
||||
),
|
||||
'index_permissions' => array( 'type' => 'keyword' ),
|
||||
'index_date_created' => array(
|
||||
'type' => 'date',
|
||||
'format' => 'epoch_second',
|
||||
),
|
||||
'index_date_updated' => array(
|
||||
'type' => 'date',
|
||||
'format' => 'epoch_second',
|
||||
),
|
||||
'index_date_commented' => array(
|
||||
'type' => 'date',
|
||||
'format' => 'epoch_second',
|
||||
),
|
||||
'index_author' => array( 'type' => 'long' ),
|
||||
'index_tags' => array( 'type' => 'keyword' ),
|
||||
'index_prefix' => array( 'type' => 'keyword' ),
|
||||
'index_hidden' => array( 'type' => 'byte' ),
|
||||
'index_item_index_id' => array( 'type' => 'keyword' ),
|
||||
'index_item_author' => array( 'type' => 'long' ),
|
||||
'index_is_last_comment' => array( 'type' => 'boolean' ),
|
||||
'index_club_id' => array( 'type' => 'long' ),
|
||||
'index_class_type_id_hash' => array( 'type' => 'keyword' ),
|
||||
'index_views' => array( 'type' => 'long' ),
|
||||
'index_comments' => array( 'type' => 'long' ),
|
||||
'index_reviews' => array( 'type' => 'long' ),
|
||||
'index_participants' => array( 'type' => 'long' ),
|
||||
)
|
||||
)
|
||||
)
|
||||
) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get index data
|
||||
*
|
||||
* @param \IPS\Content\Searchable $object Item to add
|
||||
* @return array|NULL
|
||||
*/
|
||||
public function indexData( \IPS\Content\Searchable $object )
|
||||
{
|
||||
if ( $indexData = parent::indexData( $object ) )
|
||||
{
|
||||
$indexData['index_permissions'] = explode( ',', $indexData['index_permissions'] );
|
||||
$indexData['index_tags'] = $indexData['index_tags'] ? explode( ',', $indexData['index_tags'] ) : array();
|
||||
$indexData['index_is_last_comment'] = (bool) $indexData['index_is_last_comment'];
|
||||
|
||||
if ( $object instanceof \IPS\Content\Item )
|
||||
{
|
||||
$indexData += $this->metaCounts( $object );
|
||||
}
|
||||
else
|
||||
{
|
||||
$indexData += $this->metaCounts( $object->item() );
|
||||
}
|
||||
|
||||
return $indexData;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Index an item
|
||||
*
|
||||
* @param \IPS\Content\Searchable $object Item to add
|
||||
* @return void
|
||||
*/
|
||||
public function index( \IPS\Content\Searchable $object )
|
||||
{
|
||||
if ( $indexData = $this->indexData( $object ) )
|
||||
{
|
||||
/* If nobody has permission to access it, just remove it */
|
||||
if ( !$indexData['index_permissions'] )
|
||||
{
|
||||
$this->removeFromSearchIndex( $object );
|
||||
}
|
||||
/* Otherwise, go ahead... */
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
$class = get_class( $object );
|
||||
|
||||
/* Do not allow hidden comments to be flagged as latest */
|
||||
if ( $object instanceof \IPS\Content\Comment and $indexData['index_is_last_comment'] and $indexData['index_item_id'] and $indexData['index_hidden'] !== 0 ) //
|
||||
{
|
||||
$indexData['index_is_last_comment'] = false;
|
||||
}
|
||||
|
||||
/* Insert into index */
|
||||
$r = $this->url
|
||||
->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/' . $this->getIndexId( $object ) )
|
||||
->request()
|
||||
->setHeaders( array( 'Content-Type' => 'application/json' ) )
|
||||
->put( json_encode( $indexData ) );
|
||||
|
||||
/* Views / Comments / Reviews */
|
||||
if ( $object instanceof \IPS\Content\Item )
|
||||
{
|
||||
$item = $object;
|
||||
}
|
||||
elseif ( $object instanceof \IPS\Content\Comment )
|
||||
{
|
||||
$item = $object->item();
|
||||
}
|
||||
$this->rebuildMetaCounts( $item, $indexData['index_is_last_comment'] ? $this->getIndexId( $object ) : NULL );
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'elasticsearch' );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update view count
|
||||
*
|
||||
* @param string $class Class to update
|
||||
* @param int $id ID of item
|
||||
* @param int $count Count to update
|
||||
* @throws \OutOfRangeException When table to update no longer exists
|
||||
*/
|
||||
public function updateViewCounts( $class, $id, $count )
|
||||
{
|
||||
$classes = array( $class );
|
||||
if ( isset( $class::$commentClass ) )
|
||||
{
|
||||
$classes[] = $class::$commentClass;
|
||||
}
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
{
|
||||
$classes[] = $class::$reviewClass;
|
||||
}
|
||||
|
||||
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_update_by_query' )->setQueryString( 'conflicts', 'proceed' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'script' => array(
|
||||
'inline' => "ctx._source.index_views += ". intval( $count ) . ";",
|
||||
'lang' => 'painless'
|
||||
),
|
||||
'query' => array(
|
||||
'bool' => array(
|
||||
'must' => array(
|
||||
array(
|
||||
'terms' => array(
|
||||
'index_class' => $classes
|
||||
)
|
||||
),
|
||||
array(
|
||||
'term' => array(
|
||||
'index_item_id' => $id
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the comment / review counts for an item
|
||||
*
|
||||
* @param \IPS\Content\Searchable $item The content item
|
||||
* @return void
|
||||
*/
|
||||
protected function metaCounts( $item )
|
||||
{
|
||||
$databaseColumnId = $item::$databaseColumnId;
|
||||
|
||||
$participants = array( $item->mapped('author') );
|
||||
if ( isset( $item::$commentClass ) )
|
||||
{
|
||||
$commentClass = $item::$commentClass;
|
||||
$participants += iterator_to_array( \IPS\Db::i()->select( 'DISTINCT ' . $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['author'], $commentClass::$databaseTable, array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['item'] . '=?', $item->$databaseColumnId ) ) );
|
||||
}
|
||||
if ( isset( $item::$reviewClass ) )
|
||||
{
|
||||
$reviewClass = $item::$reviewClass;
|
||||
$participants += iterator_to_array( \IPS\Db::i()->select( 'DISTINCT ' . $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['author'], $reviewClass::$databaseTable, array( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['item'] . '=?', $item->$databaseColumnId ) ) );
|
||||
}
|
||||
$participants = array_values( array_unique( $participants ) );
|
||||
|
||||
return array(
|
||||
'index_views' => $item->mapped('views'),
|
||||
'index_comments' => $item->mapped('num_comments'),
|
||||
'index_reviews' => $item->mapped('num_reviews'),
|
||||
'index_participants' => $participants
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the comment / review counts for an item
|
||||
*
|
||||
* @param \IPS\Content\Searchable $item The content item
|
||||
* @param string $lastCommentIndexId The index ID of the last comment, if you want to reset others
|
||||
* @return void
|
||||
*/
|
||||
protected function rebuildMetaCounts( $item, $lastCommentIndexId = NULL )
|
||||
{
|
||||
$databaseColumnId = $item::$databaseColumnId;
|
||||
$class = get_class( $item );
|
||||
$classes = array( $class );
|
||||
if ( isset( $class::$commentClass ) )
|
||||
{
|
||||
$classes[] = $class::$commentClass;
|
||||
}
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
{
|
||||
$classes[] = $class::$reviewClass;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$updates = array();
|
||||
foreach ( $this->metaCounts( $item ) as $k => $v )
|
||||
{
|
||||
if ( is_array( $v ) )
|
||||
{
|
||||
$updates[] = "ctx._source.{$k} = [" . implode( ',', array_map( 'intval', $v ) ) . "];";
|
||||
}
|
||||
elseif ( is_null( $v ) )
|
||||
{
|
||||
$updates[] = "ctx._source.{$k} = null;";
|
||||
}
|
||||
else
|
||||
{
|
||||
$updates[] = "ctx._source.{$k} = " . intval( $v ) . ';';
|
||||
}
|
||||
}
|
||||
if ( !is_null( $lastCommentIndexId ) )
|
||||
{
|
||||
$updates[] = "ctx._source.index_is_last_comment = ( ctx._id == '{$lastCommentIndexId}' );";
|
||||
}
|
||||
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_update_by_query' )->setQueryString( 'conflicts', 'proceed' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'script' => array(
|
||||
'inline' => implode( ' ', $updates ),
|
||||
'lang' => 'painless'
|
||||
),
|
||||
'query' => array(
|
||||
'bool' => array(
|
||||
'must' => array(
|
||||
array(
|
||||
'terms' => array(
|
||||
'index_class' => $classes
|
||||
)
|
||||
),
|
||||
array(
|
||||
'term' => array(
|
||||
'index_item_id' => $item->$databaseColumnId
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
) ) );
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'elasticsearch' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the search ID for an item
|
||||
*
|
||||
* @param \IPS\Content\Searchable $object Item to add
|
||||
* @return void
|
||||
*/
|
||||
public function getIndexId( \IPS\Content\Searchable $object )
|
||||
{
|
||||
$databaseColumnId = $object::$databaseColumnId;
|
||||
return \strtolower( str_replace( '\\', '_', \substr( get_class( $object ), 4 ) ) ) . '-' . $object->$databaseColumnId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove item
|
||||
*
|
||||
* @param \IPS\Content\Searchable $object Item to remove
|
||||
* @return void
|
||||
*/
|
||||
public function removeFromSearchIndex( \IPS\Content\Searchable $object )
|
||||
{
|
||||
try
|
||||
{
|
||||
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/' . $this->getIndexId( $object ) )->request()->delete();
|
||||
|
||||
if ( $object instanceof \IPS\Content\Item )
|
||||
{
|
||||
$class = get_class( $object );
|
||||
$idColumn = $class::$databaseColumnId;
|
||||
if ( isset( $class::$commentClass ) )
|
||||
{
|
||||
$commentClass = $class::$commentClass;
|
||||
$response = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_delete_by_query' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'query' => array(
|
||||
'bool' => array(
|
||||
'must' => array(
|
||||
array(
|
||||
'term' => array(
|
||||
'index_class' => $commentClass
|
||||
)
|
||||
),
|
||||
array(
|
||||
'term' => array(
|
||||
'index_item_id' => $object->$idColumn
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
)
|
||||
) ) );
|
||||
}
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
{
|
||||
$reviewClass = $class::$reviewClass;
|
||||
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_delete_by_query' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'query' => array(
|
||||
'bool' => array(
|
||||
'must' => array(
|
||||
array(
|
||||
'term' => array(
|
||||
'index_class' => $reviewClass
|
||||
)
|
||||
),
|
||||
array(
|
||||
'term' => array(
|
||||
'index_item_id' => $object->$idColumn
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
)
|
||||
) ) );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->rebuildMetaCounts( $object->item() );
|
||||
}
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'elasticsearch' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all content for a classs
|
||||
*
|
||||
* @param string $class The class
|
||||
* @param int|NULL $containerId The container ID to delete, or NULL
|
||||
* @param int|NULL $authorId The author ID to delete, or NULL
|
||||
* @return void
|
||||
*/
|
||||
public function removeClassFromSearchIndex( $class, $containerId=NULL, $authorId=NULL )
|
||||
{
|
||||
try
|
||||
{
|
||||
if ( $containerId or $authorId )
|
||||
{
|
||||
$query = array(
|
||||
'bool' => array(
|
||||
'must' => array(
|
||||
array(
|
||||
'term' => array(
|
||||
'index_class' => $class
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if ( $containerId )
|
||||
{
|
||||
$query['bool']['must'][] = array(
|
||||
'term' => array(
|
||||
'index_container_id' => $containerId
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( $authorId )
|
||||
{
|
||||
$query['bool']['must'][] = array(
|
||||
'term' => array(
|
||||
'index_author' => $authorId
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_delete_by_query' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'query' => $query
|
||||
) ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_delete_by_query' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'query' => array(
|
||||
'term' => array(
|
||||
'index_class' => $class
|
||||
)
|
||||
)
|
||||
) ) );
|
||||
}
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'elasticsearch' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 bool $addAuthorToPermissions If true, the index_author_id will be added to $newPermissions - used when changing the permissions for a node which allows access only to author's items
|
||||
* @return void
|
||||
*/
|
||||
public function massUpdate( $class, $containerId = NULL, $itemId = NULL, $newPermissions = NULL, $newHiddenStatus = NULL, $newContainer = NULL, $authorId = NULL, $newItemId = NULL, $newItemAuthorId = NULL, $addAuthorToPermissions = FALSE )
|
||||
{
|
||||
try
|
||||
{
|
||||
$conditions = array();
|
||||
$conditions['must'][] = array(
|
||||
'term' => array(
|
||||
'index_class' => $class
|
||||
)
|
||||
);
|
||||
if ( $containerId !== NULL )
|
||||
{
|
||||
$conditions['must'][] = array(
|
||||
'term' => array(
|
||||
'index_container_id' => $containerId
|
||||
)
|
||||
);
|
||||
}
|
||||
if ( $itemId !== NULL )
|
||||
{
|
||||
$conditions['must'][] = array(
|
||||
'term' => array(
|
||||
'index_item_id' => $itemId
|
||||
)
|
||||
);
|
||||
}
|
||||
if ( $authorId !== NULL )
|
||||
{
|
||||
$conditions['must'][] = array(
|
||||
'term' => array(
|
||||
'index_item_author' => $authorId
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$updates = array();
|
||||
if ( $newPermissions !== NULL )
|
||||
{
|
||||
$updates[] = "ctx._source.index_permissions = " . json_encode( explode( ',', $newPermissions ) ) . ";";
|
||||
}
|
||||
if ( $newContainer )
|
||||
{
|
||||
$updates[] = "ctx._source.index_container_id = " . intval( $newContainer ) . ";";
|
||||
|
||||
if ( $itemClass = ( in_array( 'IPS\Content\Item', class_parents( $class ) ) ? $class : $class::$itemClass ) and $containerClass = $itemClass::$containerNodeClass and \IPS\IPS::classUsesTrait( $containerClass, 'IPS\Content\ClubContainer' ) and $clubIdColumn = $containerClass::clubIdColumn() )
|
||||
{
|
||||
try
|
||||
{
|
||||
$updates[] = "ctx._source.index_club_id = " . intval( $containerClass::load( $newContainer )->$clubIdColumn ) . ";";
|
||||
}
|
||||
catch ( \OutOfRangeException $e )
|
||||
{
|
||||
$updates[] = "ctx._source.index_club_id = null;";
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( $newItemId )
|
||||
{
|
||||
$updates[] = "ctx._source.index_item_id = " . intval( $newItemId ) . ";";
|
||||
}
|
||||
if ( $newItemAuthorId )
|
||||
{
|
||||
$updates[] = "ctx._source.index_item_author = " . intval( $newItemAuthorId ) . ";";
|
||||
}
|
||||
|
||||
if ( count( $updates ) )
|
||||
{
|
||||
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_update_by_query' )->setQueryString( 'conflicts', 'proceed' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'script' => array(
|
||||
'inline' => implode( ' ', $updates ),
|
||||
'lang' => 'painless'
|
||||
),
|
||||
'query' => array(
|
||||
'bool' => $conditions
|
||||
)
|
||||
) ) );
|
||||
}
|
||||
|
||||
if ( $addAuthorToPermissions )
|
||||
{
|
||||
$addAuthorToPermissionsConditions = $conditions;
|
||||
$addAuthorToPermissionsConditions['must_not'][] = array(
|
||||
'term' => array(
|
||||
'index_author' => 0
|
||||
)
|
||||
);
|
||||
|
||||
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_update_by_query' )->setQueryString( 'conflicts', 'proceed' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'script' => array(
|
||||
'inline' => "ctx._source.index_permissions.add( 'm.' + ctx._source.index_author );",
|
||||
'lang' => 'painless'
|
||||
),
|
||||
'query' => array(
|
||||
'bool' => $conditions
|
||||
)
|
||||
) ) );
|
||||
}
|
||||
|
||||
if ( $newHiddenStatus !== NULL )
|
||||
{
|
||||
if ( $newHiddenStatus === 2 )
|
||||
{
|
||||
$conditions['must'][] = array(
|
||||
'term' => array(
|
||||
'index_item_author' => '0'
|
||||
)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
$conditions['must'][] = array(
|
||||
'term' => array(
|
||||
'index_item_author' => '2'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_update_by_query' )->setQueryString( 'conflicts', 'proceed' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'script' => array(
|
||||
'inline' => "ctx._source.index_permissions = " . intval( $newHiddenStatus ) . ";",
|
||||
'lang' => 'painless'
|
||||
),
|
||||
'query' => array(
|
||||
'bool' => $conditions
|
||||
)
|
||||
) ) );
|
||||
}
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'elasticsearch' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an arbitary number of elasticsearch conditions into a query
|
||||
*
|
||||
* @param array $conditions Conditions
|
||||
* @return array
|
||||
*/
|
||||
public static function convertConditionsToQuery( $conditions )
|
||||
{
|
||||
if ( count( $conditions ) == 1 )
|
||||
{
|
||||
return $conditions[0];
|
||||
}
|
||||
elseif ( count( $conditions ) == 0 )
|
||||
{
|
||||
return array( 'match_all' => new \StdClass );
|
||||
}
|
||||
else
|
||||
{
|
||||
return array(
|
||||
'bool' => array(
|
||||
'must' => array( $conditions )
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update data for the first and last comment after a merge
|
||||
* Sets index_is_last_comment on the last comment, and, if this is an item where the first comment is indexed rather than the item, sets index_title and index_tags on the first comment
|
||||
*
|
||||
* @param \IPS\Content\Item $item The item
|
||||
* @return void
|
||||
*/
|
||||
public function rebuildAfterMerge( \IPS\Content\Item $item )
|
||||
{
|
||||
if ( $item::$commentClass )
|
||||
{
|
||||
$firstComment = $item->comments( 1, 0, 'date', 'asc', NULL, FALSE, NULL, NULL, TRUE, FALSE, FALSE );
|
||||
$lastComment = $item->comments( 1, 0, 'date', 'desc', NULL, FALSE, NULL, NULL, TRUE, FALSE, FALSE );
|
||||
|
||||
$idColumn = $item::$databaseColumnId;
|
||||
$update = array( 'index_is_last_comment' => false );
|
||||
if ( $item::$firstCommentRequired )
|
||||
{
|
||||
$update['index_title'] = NULL;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/' . $this->getIndexId( $item ) . '/_update' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'doc' => $update
|
||||
) ) );
|
||||
|
||||
if ( $firstComment )
|
||||
{
|
||||
$this->index( $firstComment );
|
||||
}
|
||||
if ( $lastComment )
|
||||
{
|
||||
$this->index( $lastComment );
|
||||
}
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'elasticsearch' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prune search index
|
||||
*
|
||||
* @param \IPS\DateTime|NULL $cutoff The date to delete index records from, or NULL to delete all
|
||||
* @return void
|
||||
*/
|
||||
public function prune( \IPS\DateTime $cutoff = NULL )
|
||||
{
|
||||
if ( $cutoff )
|
||||
{
|
||||
try
|
||||
{
|
||||
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_delete_by_query' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'query' => array(
|
||||
'range' => array(
|
||||
'index_date_updated' => array(
|
||||
'lt' => $cutoff->getTimestamp()
|
||||
)
|
||||
)
|
||||
)
|
||||
) ) );
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'elasticsearch' );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->init();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the last comment flag in any given class/index_item_id
|
||||
*
|
||||
* @param array $classes The classes (when first post is required, this is typically just \IPS\forums\Topic\Post but for others, it will be both item and comment classes)
|
||||
* @param int|NULL $indexItemId The index item ID
|
||||
* @return void
|
||||
*/
|
||||
public function resetLastComment( $classes, $indexItemId )
|
||||
{
|
||||
try
|
||||
{
|
||||
/* Remove the flag */
|
||||
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_update_by_query' )->setQueryString( 'conflicts', 'proceed' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'script' => array(
|
||||
'inline' => "ctx._source.index_is_last_comment = false;",
|
||||
'lang' => 'painless'
|
||||
),
|
||||
'query' => array(
|
||||
'bool' => array(
|
||||
'must' => array(
|
||||
array(
|
||||
'terms' => array(
|
||||
'index_class' => $classes
|
||||
)
|
||||
),
|
||||
array(
|
||||
'term' => array(
|
||||
'index_item_id' => $indexItemId
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
) ) );
|
||||
|
||||
/* Get the latest comment */
|
||||
$latest = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_search' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
|
||||
'query' => array(
|
||||
'bool' => array(
|
||||
'must' => array(
|
||||
array(
|
||||
'terms' => array(
|
||||
'index_class' => $classes
|
||||
)
|
||||
),
|
||||
array(
|
||||
'term' => array(
|
||||
'index_item_id' => $indexItemId
|
||||
)
|
||||
),
|
||||
array(
|
||||
'term' => array(
|
||||
'index_hidden' => 0
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
),
|
||||
'sort' => array(
|
||||
array(
|
||||
'index_date_created' => 'desc'
|
||||
)
|
||||
),
|
||||
'size' => 1
|
||||
) ) )->decodeJson();
|
||||
|
||||
/* If we got it... */
|
||||
if ( $latest['hits']['total'] )
|
||||
{
|
||||
$result = $latest['hits']['hits'][0];
|
||||
|
||||
/* Set that it is the latest comment */
|
||||
$r = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/' . $result['_type'] . '/' . $result['_id'] . '/_update' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'doc' => array(
|
||||
'index_is_last_comment' => true
|
||||
)
|
||||
) ) );
|
||||
|
||||
/* And set the updated time on the main item (done as _update_by_query because it might not exist if the first comment is required) */
|
||||
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_update_by_query' )->setQueryString( 'conflicts', 'proceed' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'script' => array(
|
||||
'inline' => "ctx._source.index_date_updated = " . intval( $latest['_source']['index_date_updated'] ) ."; ctx._source.index_date_commented = " . intval( $latest['_source']['index_date_commented'] ) .";",
|
||||
'lang' => 'painless'
|
||||
),
|
||||
'query' => array(
|
||||
'bool' => array(
|
||||
'must' => array(
|
||||
array(
|
||||
'terms' => array(
|
||||
'index_class' => $classes
|
||||
)
|
||||
),
|
||||
array(
|
||||
'term' => array(
|
||||
'index_item_id' => $indexItemId
|
||||
)
|
||||
),
|
||||
array(
|
||||
'term' => array(
|
||||
'index_object_id' => $indexItemId
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
) ) );
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'elasticsearch' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of item index IDs, return the ones that a given member has participated in
|
||||
*
|
||||
* @param array $itemIndexIds Item index IDs
|
||||
* @param \IPS\Member $member The member
|
||||
* @return array
|
||||
*/
|
||||
public function iPostedIn( array $itemIndexIds, \IPS\Member $member )
|
||||
{
|
||||
try
|
||||
{
|
||||
$results = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_search' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
|
||||
'query' => array(
|
||||
'bool' => array(
|
||||
'filter' => array(
|
||||
array(
|
||||
'terms' => array(
|
||||
'index_item_index_id' => $itemIndexIds
|
||||
),
|
||||
),
|
||||
array(
|
||||
'term' => array(
|
||||
'index_author' => $member->member_id
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
) ) )->decodeJson();
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'elasticsearch' );
|
||||
return array();
|
||||
}
|
||||
|
||||
$iPostedIn = array();
|
||||
foreach ( $results['hits']['hits'] as $result )
|
||||
{
|
||||
$iPostedIn[] = $result['_source']['index_item_index_id'];
|
||||
}
|
||||
|
||||
return $iPostedIn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of "index_class_type_id_hash"s, return the ones that a given member has permission to view
|
||||
*
|
||||
* @param array $itemIndexIds Item index IDs
|
||||
* @param \IPS\Member $member The member
|
||||
* @return array
|
||||
*/
|
||||
public function hashesWithPermission( array $hashes, \IPS\Member $member )
|
||||
{
|
||||
try
|
||||
{
|
||||
$results = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_search' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
|
||||
'query' => array(
|
||||
'bool' => array(
|
||||
'filter' => array(
|
||||
array(
|
||||
'terms' => array(
|
||||
'index_class_type_id_hash' => $hashes
|
||||
)
|
||||
),
|
||||
array(
|
||||
'terms' => array(
|
||||
'index_permissions' => array_merge( $member->permissionArray(), array( '*' ) )
|
||||
)
|
||||
),
|
||||
array(
|
||||
'term' => array(
|
||||
'index_hidden' => 0
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
) ) )->decodeJson();
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'elasticsearch' );
|
||||
return array();
|
||||
}
|
||||
|
||||
$hashesWithPermission = array();
|
||||
foreach ( $results['hits']['hits'] as $result )
|
||||
{
|
||||
$hashesWithPermission[ $result['_source']['index_class_type_id_hash'] ] = $result['_source']['index_class_type_id_hash'];
|
||||
}
|
||||
|
||||
return $hashesWithPermission;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timestamp of oldest thing in index
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function firstIndexDate()
|
||||
{
|
||||
try
|
||||
{
|
||||
$results = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_search' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
|
||||
'size' => 1,
|
||||
'sort' => array( array( 'index_date_updated' => 'asc' ) )
|
||||
) ) )->decodeJson();
|
||||
|
||||
if ( isset( $results['hits']['hits'][0] ) )
|
||||
{
|
||||
return $results['hits']['hits'][0]['_source']['index_date_updated'];
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'elasticsearch' );
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert terms into stemmed terms for the highlighting JS
|
||||
*
|
||||
* @param array $terms Terms
|
||||
* @return array
|
||||
*/
|
||||
public function stemmedTerms( $terms )
|
||||
{
|
||||
try
|
||||
{
|
||||
$results = $this->url->setPath( '/_analyze' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
|
||||
'analyzer' => \IPS\Settings::i()->search_elastic_analyzer,
|
||||
'text' => implode( ' ', $terms )
|
||||
) ) )->decodeJson();
|
||||
|
||||
if ( isset( $results['tokens'] ) )
|
||||
{
|
||||
$stemmed = array();
|
||||
foreach ( $results['tokens'] as $token )
|
||||
{
|
||||
$stemmed[] = $token['token'];
|
||||
}
|
||||
return $stemmed;
|
||||
}
|
||||
|
||||
return $terms;
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
return $terms;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Elasticsearch Mass Indexer
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 15 Nov 2017
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Search\Elastic;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Elasticsearch Mass Indexer
|
||||
*/
|
||||
class _MassIndexer extends \IPS\Content\Search\Elastic\Index
|
||||
{
|
||||
protected $data = array();
|
||||
|
||||
/**
|
||||
* Index an item
|
||||
*
|
||||
* @param \IPS\Content\Searchable $object Item to add
|
||||
* @return void
|
||||
*/
|
||||
public function index( \IPS\Content\Searchable $object )
|
||||
{
|
||||
if ( $indexData = $this->indexData( $object ) and $indexData['index_permissions'] )
|
||||
{
|
||||
if ( $object instanceof \IPS\Content\Item )
|
||||
{
|
||||
$parent = $object;
|
||||
}
|
||||
elseif ( $object instanceof \IPS\Content\Comment )
|
||||
{
|
||||
$parent = $object->item();
|
||||
}
|
||||
|
||||
$this->data[] = array(
|
||||
'index' => array(
|
||||
'_index' => trim( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ], '/' ),
|
||||
'_type' => 'content',
|
||||
'_id' => $this->getIndexId( $object ),
|
||||
)
|
||||
);
|
||||
$this->data[] = $indexData;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit at the end of the request
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if ( $this->data )
|
||||
{
|
||||
$json = array();
|
||||
foreach ( $this->data as $data )
|
||||
{
|
||||
$json[] = json_encode( $data );
|
||||
}
|
||||
$json = implode( "\n", $json );
|
||||
|
||||
$this->url->setPath( '/_bulk' )->request()->setHeaders( array( 'Content-Type' => 'application/x-ndjson' ) )->post( $json . "\n" );
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+140
-27
@@ -30,18 +30,53 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
/**
|
||||
* Get instance
|
||||
*
|
||||
* @param bool $skipCache Do not use the cached instance if one exists
|
||||
* @return static
|
||||
*/
|
||||
public static function i()
|
||||
public static function i( $skipCache=FALSE )
|
||||
{
|
||||
if( static::$instance === NULL )
|
||||
if( static::$instance === NULL OR $skipCache === TRUE )
|
||||
{
|
||||
static::$instance = new \IPS\Content\Search\Mysql\Index;
|
||||
if ( \IPS\Settings::i()->search_method == 'elastic' )
|
||||
{
|
||||
static::$instance = new \IPS\Content\Search\Elastic\Index( \IPS\Http\Url::external( \IPS\Settings::i()->search_elastic_server . '/' . \IPS\Settings::i()->search_elastic_index ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
static::$instance = new \IPS\Content\Search\Mysql\Index;
|
||||
}
|
||||
}
|
||||
|
||||
return static::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mass indexer
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function massIndexer()
|
||||
{
|
||||
if ( \IPS\Settings::i()->search_method == 'elastic' )
|
||||
{
|
||||
return new \IPS\Content\Search\Elastic\MassIndexer( \IPS\Http\Url::external( \IPS\Settings::i()->search_elastic_server . '/' . \IPS\Settings::i()->search_elastic_index ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
return static::i();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initalize when first setting up
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
// Does nothing by default
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear and rebuild search index
|
||||
*
|
||||
@@ -96,14 +131,14 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
}
|
||||
|
||||
/* Or if this *is* the first comment, add the title and replace the tags */
|
||||
$title = $object->mapped('title');
|
||||
$title = $object->searchIndexTitle();
|
||||
$isForItem = FALSE;
|
||||
if ( $object instanceof \IPS\Content\Comment )
|
||||
{
|
||||
$itemClass = $class::$itemClass;
|
||||
if ( $itemClass::$firstCommentRequired and $object->isFirst() )
|
||||
{
|
||||
$title = $object->item()->mapped('title');
|
||||
$title = $object->item()->searchIndexTitle();
|
||||
$tags = ( $object->item() instanceof \IPS\Content\Tags and \IPS\Settings::i()->tags_enabled ) ? implode( ',', array_filter( $object->item()->tags() ) ) : NULL;
|
||||
$prefix = ( $object->item() instanceof \IPS\Content\Tags and \IPS\Settings::i()->tags_enabled ) ? $object->item()->prefix() : NULL;
|
||||
$isForItem = TRUE;
|
||||
@@ -114,10 +149,29 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
if ( $isForItem )
|
||||
{
|
||||
$dateUpdated = $object->item()->mapped('last_comment');
|
||||
$dateCommented = $object->item()->mapped('last_comment');
|
||||
}
|
||||
else
|
||||
{
|
||||
$dateUpdated = ( $object instanceof \IPS\Content\Item ) ? $object->mapped('last_comment') : $object->mapped('date');
|
||||
$dateUpdated = $object->mapped('date');
|
||||
$dateCommented = $object->mapped('date');
|
||||
if ( $object instanceof \IPS\Content\Item )
|
||||
{
|
||||
foreach ( array( 'last_comment', 'last_review', 'updated' ) as $k )
|
||||
{
|
||||
if ( $val = $object->mapped( $k ) )
|
||||
{
|
||||
if ( $val > $dateUpdated )
|
||||
{
|
||||
$dateUpdated = $val;
|
||||
}
|
||||
if ( $k != 'updated' and $val > $dateCommented )
|
||||
{
|
||||
$dateCommented = $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Is the the latest content? */
|
||||
@@ -182,7 +236,7 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
}
|
||||
|
||||
/* Strip spoilers */
|
||||
$content = $object->mapped('content');
|
||||
$content = $object->searchIndexContent();
|
||||
if ( preg_match( '#<div\s+?class=["\']ipsSpoiler["\']#', $content ) )
|
||||
{
|
||||
$content = \IPS\Text\Parser::removeElements( $content, array( 'div[class=ipsSpoiler]' ) );
|
||||
@@ -211,6 +265,12 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
{
|
||||
try
|
||||
{
|
||||
/* If the first comment is required and there is no first comment, this is a broken piece of content - do not try to index */
|
||||
if( !$object->item()->firstComment() )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$itemIndexId = $this->getIndexId( $object->item()->firstComment() );
|
||||
}
|
||||
catch ( \Exception $e ) { }
|
||||
@@ -271,24 +331,27 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
|
||||
/* Return */
|
||||
return array(
|
||||
'index_class' => $class,
|
||||
'index_object_id' => $object->$idColumn,
|
||||
'index_item_id' => ( $object instanceof \IPS\Content\Item ) ? $object->$idColumn : $object->mapped('item'),
|
||||
'index_container_class' => ( $object instanceof \IPS\Content\Item ) ? get_class( $object->searchIndexContainerClass() ) : get_class( $object->item()->searchIndexContainerClass() ),
|
||||
'index_container_id' => ( $object instanceof \IPS\Content\Item ) ? (int) $object->searchIndexContainer() : (int) $object->item()->mapped('container'),
|
||||
'index_title' => $title,
|
||||
'index_content' => $content,
|
||||
'index_permissions' => $object->searchIndexPermissions(),
|
||||
'index_date_created' => intval( $object->mapped('date') ),
|
||||
'index_date_updated' => intval( $dateUpdated ?: $object->mapped('date') ),
|
||||
'index_author' => (int) $object->mapped('author'),
|
||||
'index_tags' => $tags,
|
||||
'index_prefix' => $prefix,
|
||||
'index_hidden' => $hiddenStatus,
|
||||
'index_item_index_id' => $itemIndexId,
|
||||
'index_item_author' => intval( ( $object instanceof \IPS\Content\Item ) ? $object->mapped('author') : $object->item()->mapped('author') ),
|
||||
'index_is_last_comment' => $isLastComment,
|
||||
'index_club_id' => $clubId
|
||||
'index_class' => $class,
|
||||
'index_object_id' => $object->$idColumn,
|
||||
'index_item_id' => ( $object instanceof \IPS\Content\Item ) ? $object->$idColumn : $object->mapped('item'),
|
||||
'index_container_class' => ( $object instanceof \IPS\Content\Item ) ? ( $object->searchIndexContainerClass() ? get_class( $object->searchIndexContainerClass() ) : NULL ) : get_class( $object->item()->searchIndexContainerClass() ),
|
||||
'index_container_id' => ( $object instanceof \IPS\Content\Item ) ? (int) $object->searchIndexContainer() : (int) $object->item()->mapped('container'),
|
||||
'index_title' => $title,
|
||||
'index_content' => $content,
|
||||
'index_permissions' => $object->searchIndexPermissions(),
|
||||
'index_date_created' => intval( $object->mapped('date') ),
|
||||
'index_date_updated' => intval( $dateUpdated ),
|
||||
'index_date_commented' => intval( $dateCommented ),
|
||||
'index_author' => (int) $object->mapped('author'),
|
||||
'index_tags' => $tags,
|
||||
'index_prefix' => $prefix,
|
||||
'index_hidden' => $hiddenStatus,
|
||||
'index_item_index_id' => $itemIndexId,
|
||||
'index_item_author' => intval( ( $object instanceof \IPS\Content\Item ) ? $object->mapped('author') : $object->item()->mapped('author') ),
|
||||
'index_is_last_comment' => $isLastComment,
|
||||
'index_club_id' => $clubId,
|
||||
'index_class_type_id_hash' => md5( $class . ':' . $object->$idColumn )
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@@ -300,6 +363,19 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
*/
|
||||
abstract public function index( \IPS\Content\Searchable $object );
|
||||
|
||||
/**
|
||||
* Update view count
|
||||
*
|
||||
* @param string $class Class to update
|
||||
* @param int $id ID of item
|
||||
* @param int $count Count to update
|
||||
* @throws \OutOfRangeException When table to update no longer exists
|
||||
*/
|
||||
public function updateViewCounts( $class, $id, $count )
|
||||
{
|
||||
// Do nothing by default
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the search ID for an item
|
||||
*
|
||||
@@ -320,10 +396,11 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
* Removes all content for a classs
|
||||
*
|
||||
* @param string $class The class
|
||||
* @param int|NULL $containerId The container ID to update, or NULL
|
||||
* @param int|NULL $containerId The container ID to delete, or NULL
|
||||
* @param int|NULL $authorId The author ID to delete, or NULL
|
||||
* @return void
|
||||
*/
|
||||
abstract public function removeClassFromSearchIndex( $class, $containerId=NULL );
|
||||
abstract public function removeClassFromSearchIndex( $class, $containerId=NULL, $authorId=NULL );
|
||||
|
||||
/**
|
||||
* Removes all content for a specific application from the index (for example, when uninstalling).
|
||||
@@ -405,4 +482,40 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
* @return void
|
||||
*/
|
||||
abstract public function resetLastComment( $class, $indexItemId );
|
||||
|
||||
/**
|
||||
* Given a list of item index IDs, return the ones that a given member has participated in
|
||||
*
|
||||
* @param array $itemIndexIds Item index IDs
|
||||
* @param \IPS\Member $member The member
|
||||
* @return array
|
||||
*/
|
||||
abstract public function iPostedIn( array $itemIndexIds, \IPS\Member $member );
|
||||
|
||||
/**
|
||||
* Given a list of "index_class_type_id_hash"s, return the ones that a given member has permission to view
|
||||
*
|
||||
* @param array $itemIndexIds Item index IDs
|
||||
* @param \IPS\Member $member The member
|
||||
* @return array
|
||||
*/
|
||||
abstract public function hashesWithPermission( array $hashes, \IPS\Member $member );
|
||||
|
||||
/**
|
||||
* Get timestamp of oldest thing in index
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
abstract public function firstIndexDate();
|
||||
|
||||
/**
|
||||
* Convert terms into stemmed terms for the highlighting JS
|
||||
*
|
||||
* @param array $terms Terms
|
||||
* @return array
|
||||
*/
|
||||
public function stemmedTerms( $terms )
|
||||
{
|
||||
return $terms;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,30 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
*/
|
||||
class _Index extends \IPS\Content\Search\Index
|
||||
{
|
||||
/**
|
||||
* Get index data
|
||||
*
|
||||
* @param \IPS\Content\Searchable $object Item to add
|
||||
* @return array|NULL
|
||||
*/
|
||||
public function indexData( \IPS\Content\Searchable $object )
|
||||
{
|
||||
$indexData = parent::indexData( $object );
|
||||
|
||||
if( $indexData === NULL )
|
||||
{
|
||||
return $indexData;
|
||||
}
|
||||
|
||||
/* The index_title in core_search_index is varchar(255) so we have to limit to this length */
|
||||
if( $indexData['index_title'] !== NULL )
|
||||
{
|
||||
$indexData['index_title'] = mb_substr( $indexData['index_title'], 0, 255 );
|
||||
}
|
||||
|
||||
return $indexData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Index an item
|
||||
*
|
||||
@@ -37,9 +61,6 @@ class _Index extends \IPS\Content\Search\Index
|
||||
/* 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'] )
|
||||
{
|
||||
@@ -188,13 +209,6 @@ class _Index extends \IPS\Content\Search\Index
|
||||
\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 ) );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,17 +332,22 @@ class _Index extends \IPS\Content\Search\Index
|
||||
/**
|
||||
* Removes all content for a classs
|
||||
*
|
||||
* @param string $class The class
|
||||
* @param int|NULL $containerId The container ID to update, or NULL
|
||||
* @param string $class The class
|
||||
* @param int|NULL $containerId The container ID to delete, or NULL
|
||||
* @param int|NULL $authorId The author ID to delete, or NULL
|
||||
* @return void
|
||||
*/
|
||||
public function removeClassFromSearchIndex( $class, $containerId=NULL )
|
||||
public function removeClassFromSearchIndex( $class, $containerId=NULL, $authorId=NULL )
|
||||
{
|
||||
$where = array( array( 'index_class=?', $class ) );
|
||||
if ( $containerId !== NULL )
|
||||
{
|
||||
$where[] = array( 'index_container_id=?', $containerId );
|
||||
}
|
||||
if ( $authorId !== NULL )
|
||||
{
|
||||
$where[] = array( 'index_author=?', $authorId );
|
||||
}
|
||||
|
||||
\IPS\Db::i()->delete( 'core_search_index_item_map', array( 'index_class=? and index_item_id IN( ? )', $class, \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 ) ) );
|
||||
@@ -494,8 +513,45 @@ class _Index extends \IPS\Content\Search\Index
|
||||
\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 ) );
|
||||
\IPS\Db::i()->update( 'core_search_index', array( 'index_date_updated' => $latest['index_date_updated'], 'index_date_commented' => $latest['index_date_commented'] ), array( \IPS\Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? AND index_object_id=?', $indexItemId, $indexItemId ) );
|
||||
}
|
||||
catch( \Exception $ex ) { }
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of item index IDs, return the ones that a given member has participated in
|
||||
*
|
||||
* @param array $itemIndexIds Item index IDs
|
||||
* @param \IPS\Member $member The member
|
||||
* @return array
|
||||
*/
|
||||
public function iPostedIn( array $itemIndexIds, \IPS\Member $member )
|
||||
{
|
||||
return iterator_to_array( \IPS\Db::i()->select( 'index_item_index_id', 'core_search_index', array( array( \IPS\Db::i()->in( 'index_item_index_id', $itemIndexIds ) ), array( 'index_author=?', $member->member_id ) ) )->setKeyField('index_item_index_id') );
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of "index_class_type_id_hash"s, return the ones that a given member has permission to view
|
||||
*
|
||||
* @param array $itemIndexIds Item index IDs
|
||||
* @param \IPS\Member $member The member
|
||||
* @return array
|
||||
*/
|
||||
public function hashesWithPermission( array $hashes, \IPS\Member $member )
|
||||
{
|
||||
return iterator_to_array( \IPS\Db::i()->select( 'index_class_type_id_hash', array( 'core_search_index', 'si' ), array(
|
||||
array( "( si.index_permissions = '*' OR " . \IPS\Db::i()->findInSet( 'si.index_permissions', \IPS\Member::loggedIn()->permissionArray() ) . ' AND si.index_hidden=0 )' ),
|
||||
array( \IPS\Db::i()->in( 'si.index_class_type_id_hash', $hashes ) )
|
||||
) )->setKeyField('index_class_type_id_hash') );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timestamp of oldest thing in index
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function firstIndexDate()
|
||||
{
|
||||
return \IPS\Db::i()->select( 'MIN(index_date_updated)', 'core_search_index' )->first();
|
||||
}
|
||||
}
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
@@ -22,12 +22,14 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
*/
|
||||
abstract class _Query
|
||||
{
|
||||
const TERM_OR_TAGS = 1;
|
||||
const TERM_AND_TAGS = 2;
|
||||
const TERM_OR_MODE = 4;
|
||||
const TERM_TITLES_ONLY = 8;
|
||||
const TERM_OR_TAGS = 1; // If both tags and a term are provided, match results where either the tags or the term match
|
||||
const TERM_AND_TAGS = 2; // If both tags and a term are provided, match only results where BOTH the tags or the term match
|
||||
const TERM_TITLES_ONLY = 8; // If this flag is set, only titles will be searched
|
||||
const TAGS_MATCH_ITEMS_ONLY = 16;
|
||||
|
||||
const OPERATOR_OR = 'or';
|
||||
const OPERATOR_AND = 'and';
|
||||
|
||||
const HIDDEN_VISIBLE = 0;
|
||||
const HIDDEN_UNAPPROVED = 1;
|
||||
const HIDDEN_HIDDEN = -1;
|
||||
@@ -38,6 +40,7 @@ abstract class _Query
|
||||
const ORDER_RELEVANCY = 3;
|
||||
const ORDER_OLDEST_UPDATED = 4;
|
||||
const ORDER_OLDEST_CREATED = 5;
|
||||
const ORDER_NEWEST_COMMENTED = 6;
|
||||
|
||||
const SUPPORTS_JOIN_FILTERS = TRUE;
|
||||
|
||||
@@ -49,7 +52,14 @@ abstract class _Query
|
||||
*/
|
||||
public static function init( \IPS\Member $member = NULL )
|
||||
{
|
||||
return new \IPS\Content\Search\Mysql\Query( $member ?: \IPS\Member::loggedIn() );
|
||||
if ( \IPS\Settings::i()->search_method == 'elastic' )
|
||||
{
|
||||
return new \IPS\Content\Search\Elastic\Query( $member ?: \IPS\Member::loggedIn(), \IPS\Http\Url::external( \IPS\Settings::i()->search_elastic_server . '/' . \IPS\Settings::i()->search_elastic_index ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
return new \IPS\Content\Search\Mysql\Query( $member ?: \IPS\Member::loggedIn() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,6 +141,16 @@ abstract class _Query
|
||||
*/
|
||||
abstract public function filterByItemAuthor( \IPS\Member $author );
|
||||
|
||||
/**
|
||||
* Filter by container class
|
||||
*
|
||||
* @param array $classes Container classes to exclude from results.
|
||||
* @param array $exclude Content classes to exclude from the filter. For cases where multiple content classes may have the same container class
|
||||
* such as Gallery images, comments and reviews.
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
abstract public function filterByContainerClasses( $classes=array(), $exclude=array() );
|
||||
|
||||
/**
|
||||
* Filter by content the user follows
|
||||
*
|
||||
@@ -223,12 +243,13 @@ abstract class _Query
|
||||
/**
|
||||
* 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
|
||||
* @return \Traversable
|
||||
* @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
|
||||
* @return \IPS\Content\Search\Results
|
||||
*/
|
||||
abstract public function search( $term = NULL, $tags = NULL, $method = 1 );
|
||||
abstract public function search( $term = NULL, $tags = NULL, $method = 1, $operator = NULL );
|
||||
|
||||
/**
|
||||
* Get the default date cut off
|
||||
@@ -253,14 +274,15 @@ abstract class _Query
|
||||
/**
|
||||
* 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 \IPS\Content\Search\Index::i()->TERM_OR_TAGS or \IPS\Content\Search\Index::i()->TERM_AND_TAGS
|
||||
* @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 int
|
||||
*/
|
||||
public function count( $term = NULL, $tags = NULL, $method = 1 )
|
||||
public function count( $term = NULL, $tags = NULL, $method = 1, $operator = NULL )
|
||||
{
|
||||
return $this->search( $term, $tags, $method )->count( TRUE );
|
||||
return $this->search( $term, $tags, $method, $operator )->count( TRUE );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -285,63 +307,8 @@ abstract class _Query
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
$minLength = is_null( $minLength ) ? 0 : $minLength;
|
||||
$maxLength = is_null( $maxLength ) ? 255 : $maxLength;
|
||||
|
||||
/* Sub quotes with smaller than min word strings can cause issues for larger tables, so we convert the sub quote into normal words for a boolean search */
|
||||
if ( ! static::termIsPhrase( $term ) )
|
||||
|
||||
@@ -22,6 +22,29 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
*/
|
||||
abstract class _Result
|
||||
{
|
||||
/**
|
||||
* Pre-Display logic for search index content
|
||||
*
|
||||
* @param bool $createdDate If TRUE, uses $createdDate, otherwise uses $lastUpdatedDate
|
||||
* @return string
|
||||
*/
|
||||
public static function preDisplay( $content )
|
||||
{
|
||||
/* We cannot use images (or *anything* other than text) in search results as it messes with the truncation
|
||||
logic. All HTML (including emoticons) are already stripped before they go into the search index
|
||||
for this reason, but Emoji (because they're text) won't be. We can't swap the Emoji back out
|
||||
with one of the alternative Emoji style images though because that re-introduces images, so we
|
||||
just strip them out (making them behave like emoticons) */
|
||||
if ( \IPS\Settings::i()->emoji_style == 'twemoji' or \IPS\Settings::i()->emoji_style == 'emojione' )
|
||||
{
|
||||
return preg_replace( '/(?:' . \IPS\Text\Parser::EMOJI_REGEX . '|\x{200d})+/u', '', $content );
|
||||
}
|
||||
else
|
||||
{
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Created Date
|
||||
*/
|
||||
@@ -68,4 +91,35 @@ abstract class _Result
|
||||
return 'earlier';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get output for API
|
||||
*
|
||||
* @param \IPS\Member|NULL $authorizedMember The member making the API request or NULL for API Key / client_credentials
|
||||
* @return array
|
||||
* @apiresponse string title Title of search result
|
||||
* @apiresponse string content Content of search result
|
||||
* @apiresponse string class Content class of search result
|
||||
* @apiresponse int objectId Content ID of search result
|
||||
* @apiresponse string itemClass Content item class of search result (if search result is of a content item, this will match class)
|
||||
* @apiresponse int itemId Content item ID of search result (if search result is of a content item, this will match objectId)
|
||||
* @apiresponse datetime started Datetime search result was submitted
|
||||
* @apiresponse datetime updated Datetime search result was last updated
|
||||
* @apiresponse string itemUrl URL to content item
|
||||
* @apiresponse string objectUrl URL to search result item (if search result is of a content item, this will match itemUrl)
|
||||
* @apiresponse int reputation Number of reputation points for search result
|
||||
* @apiresponse int|null comments Number of comments or replies for search result, or NULL if commenting is not supported
|
||||
* @apiresponse int|null reviews Number of reviews for search result, or NULL if reviewing is not supported
|
||||
* @apiresponse string container Title of container of search result
|
||||
* @apiresponse string containerUrl URL to container of search result
|
||||
* @apiresponse string author Author name of search result
|
||||
* @apiresponse string|null authorUrl URL to author's profile, or NULL if search result was submitted by a guest
|
||||
* @apiresponse string authorPhoto URL to author's profile photo
|
||||
* @apiresponse string authorPhotoThumbnail URL to author's profile photo thumbnail
|
||||
* @apiresponse array tags Array of tags associated with the search result
|
||||
*/
|
||||
public function apiOutput( \IPS\Member $authorizedMember = NULL )
|
||||
{
|
||||
/* @note This is only here to populate the AdminCP reference tab - the array of search result entries is built manually */
|
||||
}
|
||||
}
|
||||
@@ -171,7 +171,7 @@ class _Results extends \ArrayIterator
|
||||
{
|
||||
$this->itemData[ $itemClass ][ $itemId ] = $itemData;
|
||||
|
||||
if ( isset( $itemClass::$databaseColumnMap ) and isset( $itemData[ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'] ] ) )
|
||||
if ( isset( $itemClass::$databaseColumnMap ) and isset( $itemClass::$databaseColumnMap['author'] ) and isset( $itemData[ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'] ] ) )
|
||||
{
|
||||
$memberId = $itemData[ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'] ];
|
||||
$membersToLoad[ $memberId ] = $memberId;
|
||||
@@ -236,16 +236,16 @@ class _Results extends \ArrayIterator
|
||||
/* Load data for the "I posted in this" stars */
|
||||
if ( count( $itemIndexIds ) and \IPS\Member::loggedIn()->member_id )
|
||||
{
|
||||
$this->iPostedIn = iterator_to_array( \IPS\Db::i()->select( 'index_item_index_id', 'core_search_index', array( array( \IPS\Db::i()->in( 'index_item_index_id', $itemIndexIds ) ), array( 'index_author=?', \IPS\Member::loggedIn()->member_id ) ) )->setKeyField('index_item_index_id') );
|
||||
$this->iPostedIn = \IPS\Content\Search\Index::i()->iPostedIn( $itemIndexIds, \IPS\Member::loggedIn() );
|
||||
}
|
||||
|
||||
/* Load tags and prefixes */
|
||||
if ( count( $itemIndexIds ) )
|
||||
if ( count( $itemIndexIds ) and \IPS\Settings::i()->search_method == 'mysql' )
|
||||
{
|
||||
$this->tags = iterator_to_array( \IPS\Db::i()->select( 'index_id, GROUP_CONCAT(index_tag) as index_tags', 'core_search_index_tags', array( array( \IPS\Db::i()->in( 'index_id', $itemIndexIds ) ), array( 'index_is_prefix = 0' ) ), NULL, NULL, 'index_id' )->setKeyField('index_id') );
|
||||
$this->prefixes = iterator_to_array( \IPS\Db::i()->select( 'index_id, index_tag as index_prefix', 'core_search_index_tags', array( array( \IPS\Db::i()->in( 'index_id', $itemIndexIds ) ), array( 'index_is_prefix = 1' ) ) )->setKeyField('index_id') );
|
||||
}
|
||||
|
||||
|
||||
/* Set that we're initiated */
|
||||
$this->initated = TRUE;
|
||||
}
|
||||
@@ -280,7 +280,8 @@ class _Results extends \ArrayIterator
|
||||
{
|
||||
/* This should not happen in production, so debug log and move on to the next item */
|
||||
\IPS\Log::debug( "index_item_id ID " . $data['index_item_id'] . " for " . $itemClass . " has no itemData" , 'searchResults' );
|
||||
return static::next();
|
||||
static::next();
|
||||
return static::current();
|
||||
}
|
||||
|
||||
$itemData = $this->itemData[ $itemClass ][ $data['index_item_id'] ];
|
||||
@@ -288,7 +289,7 @@ class _Results extends \ArrayIterator
|
||||
{
|
||||
$itemData['profile'] = $this->authorData[ $data['index_container_id'] ];
|
||||
}
|
||||
if ( isset( $itemData[ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'] ] ) )
|
||||
if ( isset( $itemClass::$databaseColumnMap['author'] ) and isset( $itemData[ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'] ] ) )
|
||||
{
|
||||
$memberId = $itemData[ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'] ];
|
||||
if ( isset( $this->authorData[ $memberId ] ) )
|
||||
@@ -301,7 +302,7 @@ class _Results extends \ArrayIterator
|
||||
{
|
||||
$data['index_tags'] = $this->tags[ $data['index_item_index_id'] ]['index_tags'];
|
||||
}
|
||||
|
||||
|
||||
if ( isset( $this->prefixes[ $data['index_item_index_id'] ]) )
|
||||
{
|
||||
$data['index_prefix'] = $this->prefixes[ $data['index_item_index_id'] ]['index_prefix'];
|
||||
@@ -432,7 +433,7 @@ class _Results extends \ArrayIterator
|
||||
/* Users registered */
|
||||
if ( in_array( 'register', $extra ) )
|
||||
{
|
||||
$where = array( array( 'joined>?', $lastTime ), array( 'name<>?', '' ) );
|
||||
$where = array( array( 'joined>?', $lastTime ), array( '( name<>? and email<>? )', '', '' ), array( 'temp_ban!=?', '-1' ) );
|
||||
if ( $firstTime )
|
||||
{
|
||||
$where[] = array( 'joined<?', $firstTime );
|
||||
@@ -441,12 +442,18 @@ class _Results extends \ArrayIterator
|
||||
{
|
||||
$where[] = array( 'core_members.member_id=?', $author->member_id );
|
||||
}
|
||||
foreach ( \IPS\Db::i()->select( 'core_members.member_id, core_members.name, core_members.members_seo_name, core_members.joined, core_validating.new_reg', 'core_members', $where, 'joined DESC', 10 )
|
||||
->join( 'core_validating', 'core_validating.member_id=core_members.member_id' ) as $member )
|
||||
{
|
||||
|
||||
foreach (
|
||||
\IPS\Db::i()->select( implode( ', ', array_merge( array_map( function( $c ) { return "core_members.{$c}"; }, \IPS\Member::columnsForPhoto() ), array( 'core_members.joined', 'core_validating.new_reg' ) ) ), 'core_members', $where, 'joined DESC', 10 )
|
||||
->join( 'core_validating', 'core_validating.member_id=core_members.member_id' )
|
||||
as $member ) {
|
||||
if ( empty( $member['new_reg'] ) )
|
||||
{
|
||||
$extraItems[] = new Result\Custom( \IPS\DateTime::ts( $member['joined'] ), \IPS\Member::loggedIn()->language()->addToStack( 'activity_member_joined', FALSE, array( 'htmlsprintf' => \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLinkFromData( $member['member_id'], $member['name'], $member['members_seo_name'] ) ) ) );
|
||||
$extraItems[] = new Result\Custom(
|
||||
\IPS\DateTime::ts( $member['joined'] ),
|
||||
\IPS\Member::loggedIn()->language()->addToStack( 'activity_member_joined', FALSE, array( 'htmlsprintf' => \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLinkFromData( $member['member_id'], $member['name'], $member['members_seo_name'] ) ) ),
|
||||
\IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userPhotoFromData( $member['member_id'], $member['name'], $member['members_seo_name'], \IPS\Member::photoUrl( $member ), 'tiny' )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -454,7 +461,7 @@ class _Results extends \ArrayIterator
|
||||
/* Users changed photo */
|
||||
if ( in_array( 'photo', $extra ) )
|
||||
{
|
||||
$where = array( array( 'photo_last_update>?', $lastTime ) );
|
||||
$where = array( array( 'photo_last_update>? AND photo_last_update>(joined+300)', $lastTime ), array( '( name<>? and email<>? )', '', '' ) );
|
||||
if ( $firstTime )
|
||||
{
|
||||
$where[] = array( 'photo_last_update<?', $firstTime );
|
||||
@@ -525,7 +532,7 @@ class _Results extends \ArrayIterator
|
||||
}
|
||||
else
|
||||
{
|
||||
$class = 'IPS\\' . $follow['follow_app'] . '\\' . ucfirst( $follow['follow_area'] );
|
||||
$class = 'IPS\\' . $follow['follow_app'] . '\\' . mb_ucfirst( $follow['follow_area'] );
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user