Seditio Source
Root |
./othercms/ips_4.3.4/system/Content/Search/Elastic/Index.php
<?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(
           
'settings'    => array(
               
'max_result_window'    => \IPS\Settings::i()->search_index_maxresults
           
),
           
'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 );
                   
                   
$resetLastComment = FALSE;                
                   
/* 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 ) //
                   
{
                       
$resetLastComment = TRUE;
                       
                       
$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' );
                }
               
                if (
$resetLastComment )
                {
                   
$this->resetLastComment( array( $indexData['index_class'] ), $indexData['index_item_id'] );
                }
            }
        }
    }
   
   
/**
     * 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( \IPS\LONG_REQUEST_TIMEOUT )->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( \IPS\LONG_REQUEST_TIMEOUT )->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
        {
           
/* Set the query */
           
$query = array(
               
'bool'    => array(
                   
'filter' => array(
                        array(
                           
'terms'    => array(
                               
'index_item_index_id' => $itemIndexIds
                           
),
                        ),
                        array(
                           
'term'    => array(
                               
'index_author' => $member->member_id
                           
)
                        )
                    )
                )
            );
           
           
/* Get the count */
           
$count = $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'    => 0,
               
'query'    => $query
           
) ) )->decodeJson();
            if ( !
$count['hits']['total'] )
            {
                return array();
            }
           
           
/* Now get the unique item ids */
           
$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(
               
'aggs'    => array(
                   
'itemIds' => array(
                       
'terms'    => array(
                           
'field'    => 'index_item_index_id',
                           
'size'    => $count['hits']['total']
                        )
                    )
                ),
               
'query'    => $query
           
) ) )->decodeJson();
        }
        catch ( \
Exception $e )
        {
            \
IPS\Log::log( $e, 'elasticsearch' );
            return array();
        }
       
       
$iPostedIn = array();
        foreach (
$results['aggregations']['itemIds']['buckets'] as $result )
        {
            if (
$result['doc_count'] )
            {
               
$iPostedIn[] = $result['key'];
            }
        }
       
        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;
        }
    }
}