<?php
/*========================================================================*\
|| ###################################################################### ||
|| # vBulletin 6.2.3 - Licence Number L3138846
|| # ------------------------------------------------------------------ # ||
|| # Copyright 2000-2026 MH Sub I, LLC dba vBulletin. All Rights Reserved.  # ||
|| # This file may not be redistributed in whole or significant part.   # ||
|| # ----------------- VBULLETIN IS NOT FREE SOFTWARE ----------------- # ||
|| # http://www.vbulletin.com | http://www.vbulletin.com/license.html   # ||
|| ###################################################################### ||
\*========================================================================*/
/**
 * @package vBulletin
 */

/**
 * @package vBulletin
 */
class vBFullTextSearch_Core extends vB_Search_Core
{
	/**************INDEXING**************************/

	/**
	 * Index a node
	 * @return bool -- true if the node was actually indexed, false otherwise.
	 */
	public function indexText($node, $title, $text, $forceupdate = false)
	{
		$nodeid = $node['nodeid'];
		return $this->indexTextBatch([$nodeid => $node], [$nodeid => $title], [$nodeid => $text], $forceupdate);
	}

	/**
	 *	Index a batch of nodes
	 *
	 *	@return bool -- true if any nodes were actually indexed, false otherwise.
	 */
	// The return value is entirely so that the indexText function can call this
	// one and determine if the single node was updated or not, which is used for
	// reindexing related nodes when a node changes.  The return is unused otherwise.
	public function indexTextBatch($nodes, $titles, $texts, $forceupdate = false)
	{
		$db = vB::getDbAssertor();

		$crc = [];
		foreach ($titles AS $nodeid => $unused)
		{
			$combined = trim($titles[$nodeid]) .  ' ' . trim($texts[$nodeid]);
			if ($combined == ' ')
			{
				//if we don't have any words update the CRC to a placeholder so we don't think the node hasn't been indexed.
				$crc[$nodeid] = 1;

				unset($nodes[$nodeid], $titles[$nodeid], $texts[$nodeid]);
			}
			else
			{
				$newcrc = sprintf('%u', crc32($combined));
				if ($nodes[$nodeid]['CRC32'] == $newcrc)
				{
					// Unless we are forcing the update we want to remove this node from the index list,
					// but there is no point in trying to update the CRC because it hasn't changed.
					if (!$forceupdate)
					{
						unset($nodes[$nodeid], $titles[$nodeid], $texts[$nodeid]);
					}
				}
				else
				{
					$crc[$nodeid] = $newcrc;
				}
			}
		}

		// if nothing changed this batch then don't try the update.  It will fail.
		if ($crc)
		{
			$db->assertQuery('vBFullTextSearch:updateNodeCrc', ['crc' => $crc]);
		}

		// if we don't have anything left to update then bail.  We might need to update
		// the crc even if we don't have anything here due to the fake crc for empty nodes.
		if (!$nodes)
		{
			return false;
		}

		// This is just copying the node text/titles to a search table.  We do this for a few reasons
		// 1) The title and text are currently in seperate tables for posts/threads.  The fulltext index requires
		// 	that all indexed fields are in the same table.
		// 2) We don't necesarily index the same text we display.  For instance we add tags to the indexed text so t
		// 	that ordinary search will return posts when the search only matches the text of a tag.  We also
		// 	add any image captions from gallery attachments to the parent post.
		// 3) We don't necesarily only generate text for nodes with an entry in the text table.
		//
		// Any attempt to reduce the need to keep a seperate copy of the text for searching is going to require more
		// infrastructure changes.
		$result = $db->assertQuery('vBFullTextSearch:insertSearchText', [
			'texts' => $texts,
			'titles' => $titles,
		]);

		return true;
	}

	public function reIndexAll()
	{
		return false;
	}

	public function emptyIndex()
	{
		parent::emptyIndex();
		$db = vB::getDbAssertor();
		$db->assertQuery('truncateTable', ['table' => 'searchfulltext']);
	}

	/**
	 *	Updates the trending post data based
	 *
	 *	@param int $lookback -- the time, in hours, that we consider threads and replies for trending
	 *	@param int $minimum -- the minimum time, in hours, that we will take as the thread lifetime.
	 *		Threads created more recently than this will be consider to have been created minimum hours
	 *		ago for purposes of scoring
	 */
	//this is unchanged from vbdbsearch.
	public function indexTrending($lookback, $minimum)
	{
		$lookback = (int) $lookback;
		$minimum = (int) $minimum;

		$db = vB::getDbAssertor();

		$channelApi = vB_Api::instance('content_channel');
		$channels = $channelApi->fetchTopLevelChannelIds();

		$channelTypeId = vB_Types::instance()->getContentTypeID('vBForum_Channel');

		$timenow = vB::getRequest()->getTimenow();
		$timecut = $timenow - ($lookback * 3600);

		$db->assertQuery('vBFullTextSearch:addNewTrendingTopics', [
			'timecut' => $timecut,
			'excludetypes' => $channelTypeId,
			'specialchannelid' => $channels['special'],
		]);

		$db->assertQuery('vBFullTextSearch:deleteOldTrendingTopics', [
			'timecut' => $timecut,
		]);

		//see comment for the query on how this works.
		$db->assertQuery('vBFullTextSearch:setTrendingWeights', [
			'timenow' => $timenow,
			'timecut' => $timecut,
			'excludetypes' => $channelTypeId,
			'minlifetime' => $minimum,
		]);
	}

	/**************DELETING**************************/

	public function delete($nodeid, $node = false)
	{
		$this->deleteBulk([$nodeid]);
	}

	public function deleteBulk($nodeids)
	{

		if (empty($nodeids))
		{
			return;
		}

		$db = vB::getDbAssertor();
		$db->delete('vBFullTextSearch:searchfulltext', ['nodeid' => $nodeids]);

		//delete any trending information for this node.
		$db->delete('vBForum:trending', ['nodeid' => $nodeids]);
	}


	/**************SEARCHING**************************/

	public function getResults(vB_Search_Criteria $criteria)
	{
		$results = $this->getTwoPassResults($criteria);
		if (is_array($results))
		{
			$nodeids = [];
			foreach ($results AS $nodeid)
			{
				$nodeids[$nodeid] = $nodeid;
			}

			return $nodeids;
		}

		$cacheKey = $results;
		$assertor = vB::getDbAssertor();

		// Check criteria before it *might* get modified by the method query.
		$equalsFilters = $criteria->get_equals_filters();
		$results = $assertor->assertQuery(
			'vBFullTextSearch:getSearchResults',
			[
				'criteria' => $criteria,
				'cacheKey' => $cacheKey,
			]
		);

		/*
			Ensure array is keyed by nodeid, like above.
			However, I'm not sure if getResults() actually needs to key the return array via
			nodeid. It *may* be needed as a way to easily de-dupe nodeids, but it seems like
			not every single return path bothered to do so, and I'm not sure if that was oversight
			or intended.
			For now, going with the more common cases of return nodeid arrays being keyed by nodeid.
		 */
		$nodeids = [];
		if ($results AND $results->valid())
		{
			foreach ($results as $result)
			{
				$nodeids[$result['nodeid']] = $result['nodeid'];
			}
			// Free memory held by result class. If buffered, this could be large.
			// Also importantly, the destruct will call the free_result() function to
			// release the database result object as soon as we no longer need it.
			unset($results);

			// Note, $cacheKey will be false if there are any post processors.
			if ($cacheKey)
			{
				// VBV-14803, allow new replies to immediately invalidate the two-pass cache.
				// This works because "channelid" in such a case is actually the topic starter, which
				// gets a node change event called upon it when a reply is added to it.
				if (!empty($equalsFilters['channelid']) AND is_numeric($equalsFilters['channelid']))
				{
					vB_Search_Core::saveSecondPassResults($nodeids, $cacheKey, $equalsFilters['channelid']);
				}
				else
				{
					vB_Search_Core::saveSecondPassResults($nodeids, $cacheKey);
				}
			}
		}

		$post_processors = $criteria->get_post_processors();
		foreach ($post_processors as $post_processor)
		{
			// nothing else to process
			if (empty($nodeids))
			{
				break;
			}
			// create new $resultset based on those node ids
			$resultset = $assertor->assertQuery(
				'vBFullTextSearch:' . $post_processor,
				[
					'nodeids' => $nodeids,
					'criteria' => $criteria
				]
			);

			$nodeids = [];
			foreach ($resultset as $result)
			{
				$nodeids[$result['nodeid']] = $result['nodeid'];
			}
			// Same deal as above, we're going to use unbuffered queries for postprocessors, so free the result object ASAP.
			unset($resultset);
		}

		return $nodeids;
	}
}

/*=========================================================================*\
|| #######################################################################
|| # Downloaded: 13:27, Sun Aug 30th 2026
|| # CVS: $RCSfile$ - $Revision: 116495 $
|| #######################################################################
\*=========================================================================*/
