Seditio Source
Root |
./othercms/ips_4.3.4/system/Content/Search/Elastic/Query.php
<?php
/**
 * @brief        Elasticsearch Search Query
 * @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        9 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 Search Query
 */
class _Query extends \IPS\Content\Search\Query
{
   
/**
     * @brief    The server URL
     */
   
protected $url;
   
   
/**
     * @brief    Filters
     */
   
protected $filters = array();
   
   
/**
     * @brief    "Must Not" filter
     */
   
protected $mustNot = array();
   
     
/**
     * @brief    The sort clause
     */
   
protected $sort = NULL;
   
   
/**
     * @brief       The offset
     */
   
protected $offset = 0;
   
   
/**
     * @brief    index_hidden statuses
     */
   
protected $hiddenStatuses = NULL;
   
   
/**
     * @brief       Item classes included
     */
   
protected $itemClasses = NULL;

   
/**
     * Constructor
     *
     * @param    \IPS\Member    $member    The member performing the search
     * @param    \IPS\Http\Url    $url    The server URL
     * @return    void
     */
   
public function __construct( \IPS\Member $member, \IPS\Http\Url $url )
    {
       
parent::__construct( $member );
       
$this->url = $url;
    }
   
   
/**
     * Filter by multiple content types
     *
     * @param    array    $contentFilters    Array of \IPS\Content\Search\ContentFilter objects
     * @param    bool    $type            TRUE means only include results matching the filters, FALSE means exclude all results matching the filters
     * @return    \IPS\Content\Search\Query    (for daisy chaining)
     */
   
public function filterByContent( array $contentFilters, $type = TRUE )
    {
       
$contentFilterConditions = array();
        if (
$type )
        {
           
$this->itemClasses = array();
        }
       
       
/* Loop through the filters... */
       
foreach ( $contentFilters as $filter )
        {
           
$conditions = array();
            if (
$type )
            {
               
$this->itemClasses[] = $filter->itemClass;
            }
           
           
/* Start by specifying the classes */
           
$conditions[] = array(
               
'terms' => array(
                   
'index_class' => $filter->classes
               
)
            );
           
           
/* Container filer */
           
if ( $filter->containerIdFilter !== NULL )
            {
                if (
$filter->containerIdFilter )
                {
                   
$conditions[] = array(
                       
'terms' => array(
                           
'index_container_id' => $filter->containerIds
                       
)
                    );
                }
                else
                {
                   
$conditions[] = array(
                       
'bool'    => array(
                           
'must_not' => array(
                               
'terms' => array(
                                   
'index_container_id' => $filter->containerIds
                               
)
                            )
                        )
                    );
                }
            }
           
           
/* Item filter */
           
if ( $filter->itemIdFilter !== NULL )
            {
                if (
$filter->itemIdFilter )
                {
                   
$conditions[] = array(
                       
'terms' => array(
                           
'index_item_id' => $filter->itemIds
                       
)
                    );
                }
                else
                {
                   
$conditions[] = array(
                       
'bool'    => array(
                           
'must_not' => array(
                               
'terms' => array(
                                   
'index_item_id' => $filter->itemIds
                               
)
                            )
                        )
                    );
                }
            }
            if (
$filter->objectIdFilter !== NULL )
            {
                if (
$filter->objectIdFilter )
                {
                   
$conditions[] = array(
                       
'terms' => array(
                           
'index_object_id' => $filter->objectIds
                       
)
                    );
                }
                else
                {
                   
$conditions[] = array(
                       
'bool'    => array(
                           
'must_not' => array(
                               
'terms' => array(
                                   
'index_object_id' => $filter->objectIds
                               
)
                            )
                        )
                    );
                }
            }
           
           
/* Minimum views / comments / reviews */
           
foreach ( array( 'minimumViews' => 'index_views', 'minimumComments' => 'index_comments', 'minimumReviews' => 'index_reviews' ) as $filterKey => $indexKey )
            {
                if (
$filter->$filterKey )
                {
                   
$conditions[] = array(
                       
'range' => array(
                           
$indexKey => array( 'gte' => $filter->$filterKey )
                        )
                    );
                }
            }
           
           
/* Only first comment? */
           
if ( $filter->onlyFirstComment )
            {
               
$conditions[] = array(
                   
'exists' => array( 'field' => 'index_title' )
                );
            }
           
           
/* Only last comment? */
           
if ( $filter->onlyLastComment )
            {
               
$conditions[] = array(
                   
'term' => array( 'index_is_last_comment' => true )
                );
            }
           
           
/* Put it together */
           
$contentFilterConditions[] = Index::convertConditionsToQuery( $conditions );
        }
       
       
/* Put them together */
       
if ( count( $contentFilterConditions ) > 1 )
        {        
           
$this->filters[] = array(
               
'bool'    => array(
                    (
$type ? 'should' : 'must_not' ) => array( $contentFilterConditions )
                )
            );
        }
        elseif (
$type )
        {
           
$this->filters[] = $contentFilterConditions[0];
        }
        else
        {
           
$this->mustNot[] = $contentFilterConditions[0];
        }
       
        return
$this;
    }
       
   
/**
     * Filter by author
     *
     * @param    \IPS\Member|int|array    $author                        The author, or an array of author IDs
     * @return    \IPS\Content\Search\Query    (for daisy chaining)
     */
   
public function filterByAuthor( $author )
    {
        if (
is_array( $author ) )
        {
           
$this->filters[] = array( 'terms' => array( 'index_author' => $author ) );
        }
        else
        {
           
$this->filters[] = array( 'term' => array( 'index_author' => $author instanceof \IPS\Member ? $author->member_id : $author ) );
        }
       
        return
$this;
    }
   
   
/**
     * Filter by club
     *
     * @param    \IPS\Member\Club|int|array    $club    The club, or array of club IDs
     * @return    \IPS\Content\Search\Query    (for daisy chaining)
     */
   
public function filterByClub( $club )
    {
        if (
$club === NULL )
        {
           
$this->mustNot[] = array(
               
'exists' => array( 'field' => 'index_club_id' )
            );
        }
        elseif (
is_array( $club ) )
        {
           
$this->filters[] = array(
               
'terms' => array( 'index_club_id' => $club )
            );
        }
        else
        {
           
$this->filters[] = array(
               
'term' => array( 'index_club_id' => $club instanceof \IPS\Member\Club ? $club->id : $club )
            );
        }
       
        return
$this;
    }
   
   
/**
     * Filter for profile
     *
     * @param    \IPS\Member    $member    The member whose profile is being viewed
     * @return    \IPS\Content\Search\Query    (for daisy chaining)
     */
   
public function filterForProfile( \IPS\Member $member )
    {
       
/* Filter by content they've posted or posts on their wall */
       
$this->filters[] = array(
           
'bool' => array(
               
'should' => array(
                    array(
                       
'term' => array( 'index_author' => $member->member_id )
                    ),
                    array(
                       
'bool' => array(
                           
'filter' => array(
                                array(
                                   
'term' => array(
                                       
'index_class' => 'IPS\core\Statuses\Status'
                                   
)
                                ),
                                array(
                                   
'term' => array(
                                       
'index_container_id' => $member->member_id
                                   
)
                                )
                            )
                        )
                    )
                )
            )
        );
       
       
/* Get the list of valid classes */
       
foreach ( \IPS\Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
        {
            foreach (
$object->classes as $class )
            {
                if (
in_array( 'IPS\Content\Item', class_parents( $class ) ) )
                {
                   
$classesChecked[]    = $class;
                }
            }
        }

       
/* Give content item classes a chance to inspect and manipulate filters */
       
$filters = array();
        foreach(
$classesChecked as $itemClass )
        {
           
$itemClass::searchEngineFiltering( $filters, $this );
        }
       
       
/* Return for daisy chaining */
       
return $this;
    }
   
   
/**
     * 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)
     */
   
public function filterByContainerClasses( $classes=array(), $exclude=array() )
    {
        if( empty(
$exclude ) )
        {
           
$this->filters[] = array(
               
'bool'    => array(
                   
'must_not' => array(
                       
'terms' => array( 'index_container_class' => $classes )
                    )
                )
            );
        }
        elseif (
$classes )
        {
           
$this->filters[] = array(
               
'bool'    => array(
                   
'should' => array(
                        array(
                           
'bool' => array(
                               
'must_not' => array(
                                   
'terms' => array( 'index_container_class' => $classes ),
                                )
                            )
                        ),
                        array(
                           
'terms' => array( 'index_container_class' => $exclude ),
                        )
                    )
                )
            );
        }

         
        return
$this;
    }
   
   
/**
     * Filter by item author
     *
     * @param    \IPS\Member    $author        The author
     * @return    \IPS\Content\Search\Query    (for daisy chaining)
     */
   
public function filterByItemAuthor( \IPS\Member $author )
    {
       
$this->filters[] = array(
           
'term' => array( 'index_item_author' => $author->member_id )
        );
        return
$this;
    }
   
   
/**
     * Filter by content the user follows
     *
     * @param    bool    $includeContainers    Include content in containers the user follows?
     * @param    bool    $includeItems        Include items and comments/reviews on items the user follows?
     * @param    bool    $includeContainers    Include content posted by members the user follows?
     * @return    \IPS\Content\Search\Query    (for daisy chaining)
     */
   
public function filterByFollowed( $includeContainers, $includeItems, $includeMembers )
    {
       
$conditions = array();
       
$followApps = $followAreas = $case = $containerCase = array();
       
$followedItems        = array();
       
$followedContainers    = array();

       
/* Are we including items or containers? */
       
if ( $includeContainers or $includeItems )
        {
           
/* Work out what classes we need to examine */
           
if ( $this->itemClasses !== NULL )
            {
               
$classes = $this->itemClasses;
            }
            else
            {
               
$classes = array();
                foreach ( \
IPS\Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
                {
                   
$classes = array_merge( $object->classes, $classes );
                }
            }
           
           
/* Loop them */
           
foreach ( $classes as $class )
            {
                if(
is_subclass_of( $class, 'IPS\Content\Followable' ) )
                {
                   
$followApps[ $class::$application ] = $class::$application;
                   
$followArea = mb_strtolower( mb_substr( $class, mb_strrpos( $class, '\\' ) + 1 ) );
                   
                    if (
$includeContainers and $includeItems )
                    {
                       
$followAreas[] = mb_strtolower( mb_substr( $class::$containerNodeClass, mb_strrpos( $class::$containerNodeClass, '\\' ) + 1 ) );
                       
$followAreas[] = $followArea;
                    }
                    elseif (
$includeItems )
                    {
                       
$followAreas[] = $followArea;
                    }
                    elseif (
$includeContainers )
                    {
                       
$followAreas[] = mb_strtolower( mb_substr( $class::$containerNodeClass, mb_strrpos( $class::$containerNodeClass, '\\' ) + 1 ) );
                    }
                   
                   
/* Work out what classes this applies to - need to specify comment and review classes */
                   
if ( ! $class::$firstCommentRequired )
                    {
                       
$case[ $followArea ][] = $class;
                    }
                   
                    if(
$includeContainers )
                    {
                       
$containerCase[ $followArea ] = mb_strtolower( mb_substr( $class::$containerNodeClass, mb_strrpos( $class::$containerNodeClass, '\\' ) + 1 ) ) ;
                    }
                   
                    if ( isset(
$class::$commentClass ) )
                    {
                       
$case[ $followArea ][] = $class::$commentClass;
                    }
                    if ( isset(
$class::$reviewClass ) )
                    {
                       
$case[ $followArea ][] = $class::$reviewClass;
                    }
                }
            }

           
/* Get the stuff we follow */
           
foreach( \IPS\Db::i()->select( '*', 'core_follow', array( 'follow_member_id=? AND ' . \IPS\Db::i()->in( 'follow_app', $followApps ) . ' AND ' . \IPS\Db::i()->in( 'follow_area', $followAreas ), $this->member->member_id ) ) as $follow )
            {
                if(
array_key_exists( $follow['follow_area'], $case ) )
                {
                   
$followedItems[ $follow['follow_area'] ][]    = $follow['follow_rel_id'];
                }
                else if(
in_array( $follow['follow_area'], $containerCase ) )
                {
                   
$followedContainers[ $follow['follow_area'] ][]    = $follow['follow_rel_id'];
                }
            }
        }

        foreach(
$followedItems as $area => $item )
        {
           
$conditions[] = array(
               
'bool' => array(
                   
'filter' => array(
                        array(
                           
'terms'    => array( 'index_class' =>  $case[ $area ] )
                        ),
                        array(
                           
'terms'    => array( 'index_item_id' =>  $item )
                        ),
                    )
                )
            );
        }

        foreach(
$followedContainers as $area => $container )
        {
           
$indexClasses    = array();

            foreach(
$containerCase as $followArea => $containerArea )
            {
                if(
$containerArea == $area )
                {
                   
$indexClasses    = $case[ $followArea ];
                }
            }
           
           
$conditions[] = array(
               
'bool' => array(
                   
'filter' => array(
                        array(
                           
'terms'    => array( 'index_class' =>  $indexClasses )
                        ),
                        array(
                           
'terms'    => array( 'index_container_id' =>  $container )
                        ),
                    )
                )
            );
        }
       
       
/* Are we including content posted by followed members? */
       
if ( $includeMembers and $followed = iterator_to_array( \IPS\Db::i()->select( 'follow_rel_id', 'core_follow', array( 'follow_app=? AND follow_area=? AND follow_member_id=?', 'core', 'member', $this->member->member_id ), 'follow_rel_id asc' ) ) )
        {
           
$conditions[] = array(
                array(
                   
'terms'    => array( 'index_author' =>  $followed )
                )
            );            
        }
       
       
/* Put it all together */
       
if ( count( $conditions ) )    
        {
           
$this->filters[] = array( 'bool' => array( 'should' => $conditions ) );
        }
        else
        {
           
$this->filters[] = array( 'match_none' => new \StdClass );
        }

       
/* And return */
       
return $this;
    }
   
   
/**
     * Filter by content the user has posted in
     *
     * @return    \IPS\Content\Search\Query    (for daisy chaining)
     */
   
public function filterByItemsIPostedIn()
    {
       
$this->filters[] = array(
           
'term'            => array(
               
'index_participants'    => $this->member->member_id
           
)
        );
        return
$this;
    }
   
   
/**
     * Filter by content the user has not read
     *
     * @note    If applicable, it is more efficient to call filterByContent() before calling this method
     * @return    \IPS\Content\Search\Query    (for daisy chaining)
     */
   
public function filterByUnread()
    {
       
/* Work out what classes we need to examine */
       
if ( $this->itemClasses !== NULL )
        {
           
$classes = $this->itemClasses;
        }
        else
        {
           
$classes = array();
            foreach ( \
IPS\Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
            {
               
$classes = array_merge( $object->classes, $classes );
            }
        }
       
       
/* Loop them */
       
$conditions = array();
       
$resetTimes = $this->member->markersResetTimes( NULL );
        foreach (
$classes as $class )
        {
            if(
is_subclass_of( $class, 'IPS\Content\ReadMarkers' ) )
            {
               
$containerClass = ( $class::$containerNodeClass ) ? $class::$containerNodeClass : NULL;
               
$classConditions = array();
               
               
/* Work out what classes this applies to - need to specify comment and review classes */
               
$_classes = array( $class );
                if ( isset(
$class::$commentClass ) )
                {
                   
$_classes[] = $class::$commentClass;
                }
                if ( isset(
$class::$reviewClass ) )
                {
                   
$_classes[] = $class::$reviewClass;
                }
               
$classConditions[] = array(
                   
'terms' => array( 'index_class' => $_classes )
                );
               
               
/* Get the reset times */
               
$classBits = explode( "\\", $class );
               
$application = $classBits[1];
               
$containerConditions = array();
               
$markers = array();
                if ( isset(
$resetTimes[ $application ] ) )
                {
                    foreach(
$resetTimes[ $application ] as $containerId => $timestamp )
                    {
                       
/* Pages has different classes per database, but recorded as 'cms' and the container ID in the marking tables */
                       
if ( $containerClass and method_exists( $containerClass, 'isValidContainerId' ) )
                        {
                            if ( !
$containerClass::isValidContainerId( $containerId ) )
                            {
                                continue;
                            }
                        }
                       
                       
/* Add a condition to exlude anything in this container since the last time we marked the whole thing read */
                       
$timestamp = $timestamp ?: $this->member->marked_site_read;
                       
$containerConditions[ $containerId ] = array(
                           
'bool' => array(
                               
'filter' => array(
                                    array(
                                       
'term' => array( 'index_container_id' => $containerId )
                                    ),
                                    array(
                                       
'range' => array( 'index_date_updated' => array( 'gt' => $timestamp ) )
                                    )
                                )
                            )
                        );
                       
                       
/* And get the times each individual item was read for later */
                       
$items = $this->member->markersItems( $application, \IPS\Content\Item::makeMarkerKey( $containerId ) );
                        if (
count( $items ) )
                        {
                            foreach(
$items as $mid => $mtime )
                            {
                                if (
$mtime > $timestamp )
                                {
                                   
/* If an item has been moved from one container to another, the user may have a marker
                                        in it's old location, with the previously 'read' time. In this circumstance, we need
                                        to only use more recent read time, otherwise the topic may be incorrectly included
                                        in the results */
                                   
if ( in_array( $mid, $markers ) )
                                    {
                                       
$_key = array_search( $mid, $markers );
                                       
$_mtime = intval( mb_substr( $_key, 0, mb_strpos( $_key, '.' ) ) );
                                        if (
$_mtime < $mtime )
                                        {
                                            unset(
$markers[ $_key ] );
                                        }
                                       
/* If the existing timestamp is higher, retain that since we reset the $markers array below */
                                       
else
                                        {
                                           
$mtime = $_mtime;
                                        }
                                    }
                                   
                                   
$markers[ $mtime . '.' . $mid ] = $mid;
                                }
                            }
                        }
                    }
                }
                if (
$containerConditions )
                {
                   
$containerConditions[] = array(
                       
'bool' => array(
                           
'must_not' => array(
                               
'terms' => array( 'index_container_id' => array_keys( $containerConditions ) )
                            ),
                           
'filter' => array(
                               
'range' => array( 'index_date_updated' => array( 'gt' => $this->member->marked_site_read ) )
                            )
                        )
                    );
                   
                   
$classConditions[] = array(
                       
'bool' => array(
                           
'should' => array_values( $containerConditions )
                        )
                    );
                }
                else
                {
                   
$classConditions[] = array(
                       
'range' => array( 'index_date_updated' => array( 'gt' => $this->member->marked_site_read ) )
                    );
                }
               
               
$notIn  = array();
                if (
count( $markers ) )
                {
                   
$useIds = array_flip( $markers );
                   
                   
$dateColumns = array();
                    foreach ( array(
'updated', 'last_comment', 'last_review' ) as $k )
                    {
                        if ( isset(
$class::$databaseColumnMap[ $k ] ) )
                        {
                            if (
is_array( $class::$databaseColumnMap[ $k ] ) )
                            {
                                foreach (
$class::$databaseColumnMap[ $k ] as $v )
                                {
                                   
$dateColumns[] = " IFNULL( " . $class::$databaseTable . '.'. $class::$databasePrefix . $v . ", 0 )";
                                }
                            }
                            else
                            {
                               
$dateColumns[] = " IFNULL( " . $class::$databaseTable . '.'. $class::$databasePrefix . $class::$databaseColumnMap[ $k ] . ", 0 )";
                            }
                        }
                    }
                   
$dateColumnExpression = count( $dateColumns ) > 1 ? ( 'GREATEST(' . implode( ',', $dateColumns ) . ')' ) : array_pop( $dateColumns );
                   
                    foreach( \
IPS\Db::i()->select( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnId. ' as _id, ' . $dateColumnExpression . ' as _date', $class::$databaseTable, \IPS\Db::i()->in( $class::$databasePrefix . $class::$databaseColumnId, array_keys( $useIds ) ) ) as $row )
                    {
                        if ( isset(
$useIds[ $row['_id'] ] ) )
                        {
                            if (
$useIds[ $row['_id'] ] >= $row['_date'] )
                            {
                               
/* Still read */
                               
$notIn[] = intval( $row['_id'] );
                            }
                        }
                    }
                }
               
               
/* Add it to the array */
               
$_condition = array(
                   
'bool' => array(
                       
'filter' => $classConditions
                   
)
                );
                if (
count( $notIn ) )
                {
                   
$_condition['bool']['must_not'] = array(
                       
'terms' => array(
                           
'index_item_id' => $notIn
                       
)
                    );
                }
               
$conditions[] = $_condition;
            }
        }        
       
       
/* Put it all together */
       
if ( count( $conditions ) )
        {
           
$this->filters[] = array(
               
'bool' => array(
                   
'should' => $conditions
               
)
            );
        }
                       
        return
$this;
    }
   
   
/**
     * Filter by start date
     *
     * @param    \IPS\DateTime|NULL    $start        The start date (only results AFTER this date will be returned)
     * @param    \IPS\DateTime|NULL    $end        The end date (only results BEFORE this date will be returned)
     * @return    \IPS\Content\Search\Query    (for daisy chaining)
     */
   
public function filterByCreateDate( \IPS\DateTime $start = NULL, \IPS\DateTime $end = NULL )
    {
       
$range = array();
       
        if (
$start )
        {
           
$range['gt'] = $start->getTimestamp();
        }
        if (
$end )
        {
           
$range['lt'] = $end->getTimestamp();
        }
       
        if (
$range )
        {
           
$this->filters[] = array(
               
'range' => array( 'index_date_created' => $range )
            );
        }
       
        return
$this;
    }
   
   
/**
     * Filter by last updated date
     *
     * @param    \IPS\DateTime|NULL    $start        The start date (only results AFTER this date will be returned)
     * @param    \IPS\DateTime|NULL    $end        The end date (only results BEFORE this date will be returned)
     * @return    \IPS\Content\Search\Query    (for daisy chaining)
     */
   
public function filterByLastUpdatedDate( \IPS\DateTime $start = NULL, \IPS\DateTime $end = NULL )
    {
       
$range = array();
       
        if (
$start )
        {
           
$range['gt'] = $start->getTimestamp();
        }
        if (
$end )
        {
           
$range['lt'] = $end->getTimestamp();
        }
       
        if (
$range )
        {
           
$this->filters[] = array(
               
'range' => array( 'index_date_updated' => $range )
            );
        }
       
        return
$this;
    }
   
   
/**
     * Set hidden status
     *
     * @param    int|array|NULL    $statuses    The statuses (see HIDDEN_ constants) or NULL for any
     * @return    \IPS\Content\Search\Query    (for daisy chaining)
     */
   
public function setHiddenFilter( $statuses )
    {
       
$this->hiddenStatuses = $statuses;
        return
$this;
    }
       
   
/**
     * Set page
     *
     * @param    int        $page    The page number
     * @return    \IPS\Content\Search\Query    (for daisy chaining)
     */
   
public function setPage( $page )
    {
       
$this->offset = ( $page - 1 ) * $this->resultsToGet;
       
        return
$this;
    }
   
   
/**
     * Set order
     *
     * @param    int        $order    Order (see ORDER_ constants)
     * @return    \IPS\Content\Search\Query    (for daisy chaining)
     */
   
public function setOrder( $order )
    {
        switch (
$order )
        {
            case static::
ORDER_NEWEST_UPDATED:
               
$this->sort = array( array( 'index_date_updated' => 'desc' ) );
                break;
               
            case static::
ORDER_OLDEST_UPDATED:
               
$this->sort = array( array( 'index_date_updated' => 'asc' ) );
                break;
           
            case static::
ORDER_NEWEST_CREATED:
               
$this->sort = array( array( 'index_date_created' => 'desc' ) );
                break;
               
            case static::
ORDER_OLDEST_CREATED:
               
$this->sort = array( array( 'index_date_created' => 'asc' ) );
                break;
               
            case static::
ORDER_NEWEST_COMMENTED:
               
$this->sort = array( array( 'index_date_commented' => 'desc' ) );
                break;

            case static::
ORDER_RELEVANCY:
               
$this->sort = NULL;
                break;
        }
       
        return
$this;
    }
       
   
/**
     * Search
     *
     * @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
     */
   
public function search( $term = NULL, $tags = NULL, $method = 1, $operator = NULL )
    {
       
$operator = $operator ?: \IPS\Settings::i()->search_default_operator;
       
$must = array();
       
$filters = $this->filters;
       
       
/* Set our conditions for this search */
       
if ( $term !== NULL or $tags !== NULL )
        {
           
$searchConditions = array();
           
$titleField = \IPS\Settings::i()->search_title_boost ? ( 'index_title^' . intval( \IPS\Settings::i()->search_title_boost ) ) : 'index_title';
           
           
/* Build the condition for the search term */
           
if ( $term !== NULL )
            {
                if ( static::
termIsPhrase( $term ) )
                {
                   
$term = trim( $term, '"' );
                    if (
$method & static::TERM_TITLES_ONLY )
                    {
                       
$searchConditions[] = array( 'match_phrase' => array( 'index_title' => array( 'query' => $term, 'analyzer' => 'standard' ) ) );
                    }
                    else
                    {
                       
$searchConditions[] = array( 'multi_match' => array( 'query' => $term, 'fields' => array( 'index_content', $titleField ), 'type' => 'phrase', 'analyzer' => 'standard' ) );
                    }
                }
                else
                {
                    if (
$method & static::TERM_TITLES_ONLY )
                    {
                       
$searchConditions[] = array( 'match' => array( 'index_title' => array( 'query' => $term, 'operator' => $operator ) ) );
                    }
                    else
                    {
                       
$searchConditions[] = array( 'multi_match' => array( 'query' => $term, 'fields' => array( 'index_content', $titleField ), 'operator' => $operator ) );
                    }
                }
            }
           
/* Build the condition for the tags */
           
if ( $tags !== NULL )
            {
               
$searchConditions[] = array(
                   
'bool' => array(
                       
'should' => array(
                            array(
                               
'terms' => array( 'index_tags' => $tags )
                            ),
                            array(
                               
'terms'    => array( 'index_prefix' => $tags )
                            )
                        )
                    )
                );
            }
           
           
/* Put that with the rest of the conditions */
           
if ( $term !== NULL and $tags !== NULL )
            {
                if (
$method & static::TERM_OR_TAGS )
                {
                   
$must[] = array( 'bool' => array( 'should' => $searchConditions ) );
                }
                else
                {
                   
$must = $searchConditions;
                }
            }
            else
            {
               
$must[] = $searchConditions[0];
            }
        }
       
       
/* Only get stuff we have permission for */
       
$filters[] = array( 'terms' => array( 'index_permissions' => array_merge( $this->permissionArray(), array( '*' ) ) ) );
        if (
$this->hiddenStatuses !== NULL )
        {
            if (
is_array( $this->hiddenStatuses ) )
            {
               
$filters[] = array( 'terms' => array( 'index_hidden' => $this->hiddenStatuses ) );
            }
            else
            {
               
$filters[] = array( 'term' => array( 'index_hidden' => $this->hiddenStatuses ) );
            }
        }
       
       
/* Peform the search */
       
try
        {
           
/* Initial query */
           
$query = array(
               
'bool'    => array(
                   
'must'        => $must,
                   
'must_not'    => $this->mustNot,
                   
'filter'    => $filters
               
)
            );
           
           
/* Add the time decay */
           
if ( \IPS\Settings::i()->search_decay_factor and !$this->sort )
            {
               
$query = array(
                   
'function_score' => array(
                       
'query'            => $query,
                       
'linear'            => array(
                           
'index_date_updated' => array(
                               
'scale'                => intval( \IPS\Settings::i()->search_decay_days ) . 'd',
                               
'decay'                => number_format( \IPS\Settings::i()->search_decay_factor, 1, '.', '' )
                            )
                        )
                    )
                );
            }
           
           
/* Add the self boost */
           
if ( \IPS\Settings::i()->search_elastic_self_boost and $this->member->member_id )
            {
               
$query = array(
                   
'function_score' => array(
                       
'query'            => $query,
                       
'script_score'        => array(
                           
'script'            => array(
                               
'inline'            => "doc['index_author'].value == " . intval( $this->member->member_id ) . " ? ( _score * " . number_format( \IPS\Settings::i()->search_elastic_self_boost, 1, '.', '' ) . " ) : _score",
                               
'lang'                => 'painless'
                           
)
                        )
                    )
                );
            }
           
           
/* Build the JSON and validate it. Use JSON_PARTIAL_OUTPUT_ON_ERROR in case someone has used an unencodable value as
                the term, and check json_encode() didn't return FALSE (which it may do on error) as a sanity check against
                sending a blank query, which would return everything in the index */
           
$array = array(
               
'query'    => $query,
               
'sort'    => $this->sort ?: array(),
               
'from'    => $this->offset,
               
'size'    => $this->resultsToGet,
            );
           
$json = json_encode( $array, JSON_PARTIAL_OUTPUT_ON_ERROR );
            if (
$json === FALSE )
            {                
                return new \
IPS\Content\Search\Results( array(), 0 );
            }
           
           
/* Make the call! */
           
$return = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_search' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( $json )->decodeJson();
            if ( isset(
$return['error'] ) )
            {
                \
IPS\Log::log( print_r( $return['error'], TRUE ), 'elasticsearch' );
                return new \
IPS\Content\Search\Results( array(), 0 );
            }
           
           
/* Set results */            
           
return new \IPS\Content\Search\Results( array_map( function( $hit ) {
               
$indexData = $hit['_source'];
               
$indexData['index_permissions'] = implode( ',', $indexData['index_permissions'] );
               
$indexData['index_tags'] = $indexData['index_tags'] ? implode( ',', $indexData['index_tags'] ) : NULL;
                return
$indexData;
            },
$return['hits']['hits'] ), $return['hits']['total'] <= \IPS\Settings::i()->search_index_maxresults ? $return['hits']['total'] : (int) \IPS\Settings::i()->search_index_maxresults );
        }
        catch ( \
Exception $e )
        {
            \
IPS\Log::log( $e, 'elasticsearch' );
            return new \
IPS\Content\Search\Results( array(), 0 );
        }
    }
}