• This site uses cookies. By continuing to use this site, you are agreeing to our use of cookies. Learn more.

Similar Threads v1.3

AndyB

Well-known member
#1
Purpose:

The purpose of this thread is to provide additional information on the Similar Threads v1.3 add-on. To download and install this add-on please visit the following XenForo Resource:

http://xenforo.com/community/resources/similar-threads.2441/

Description:

The Similar Threads add-on will do the following:
  • Show a list of similar threads when a user is creating a new thread
  • Show a list of similar threads when a user is viewing a thread
Key Features:
  • Six admin options that allow you to customize this add-on
  • Node view permissions honored
  • Results immediately displayed
  • Automatically disabled in responsive mode
  • Fully phrased
Results Example:

pic001.jpg

Admin Options:

pic002.jpg
 

AndyB

Well-known member
#2
The directory structure with the core files highlighted.

js
--xenforo
----similarthreads.js
library
--Andy
----SimilarThreads
------ControllerPublic
--------Abstract.php
--------Thread.php
------Model.php
------Route
--------Prefix
----------SimilarThreads.php
 

AndyB

Well-known member
#3
js/xenforo/similarthreads.js

Code:
!function($, window, document, _undefined)
{
	XenForo.similarthreadsId = function($form)
	{		
		var typewatch = (function()
		{
			var timer = 0;
			return function(callback, ms)
			{
				clearTimeout (timer);
				timer = setTimeout(callback, ms);
			}  
		})(); 
		$title = $form.find('input[name="title"]');
		$title.keyup(function() 
		{
			typewatch(function () 
			{
				XenForo.ajax(
				$('base').attr('href') + 'similarthreads/',
				$form.serializeArray(),
				function(ajaxData, textStatus)
				{
					if (ajaxData.templateHtml)
					{
						new XenForo.ExtLoader(ajaxData, function()
						{
							$('#similarthreadsId-result').html('<div>' + ajaxData.templateHtml + '</div>');
						});
					}
				});
			}, 500);
		});		
	}	
	XenForo.register('#similarthreadsId', 'XenForo.similarthreadsId');	
}
(jQuery, this, document);
 

AndyB

Well-known member
#4
library/Andy/SimilarThreads/ControllerPublic/Abstract.php

PHP:
<?php

class Andy_SimilarThreads_ControllerPublic_Abstract extends XenForo_ControllerPublic_Abstract
{
    public function actionIndex()
    { 
        // get options from Admin CP -> Options -> Similar Threads -> Show on new thread
        $showCreateThread = XenForo_Application::get('options')->showCreateThread;
	
		// show similar threads if true
		if ($showCreateThread)
		{
			// declare variables
			$currentThreadId = '';
			$similarThreads = array();
			$searchWords = array();
			$searchWord1 = '';
			$searchWord2 = '';			
			$safeSearchWord1 = '';
			$safeSearchWord2 = '';
				
			// get newTitle
			$newTitle = $this->_request->getParam('title');
	
			// put into array
			$newTitle = explode(' ', $newTitle);
			
			// get options from Admin CP -> Options -> Similar Threads -> Common Words    
			$commonWords = XenForo_Application::get('options')->commonWords;
			
			// convert to lowercase
			$commonWords = strtolower($commonWords);    
			
			// put $commonWordsLower into an array
			$commonWords = explode(' ', $commonWords); 
			
			// remove any common words from array
			foreach($newTitle as $var)
			{
				if (!in_array(strtolower($var), $commonWords))
				{
					// get options from Admin CP -> Options -> Similar Threads -> Miniumum Common Word Length    
					$minimumCommonWordLength = XenForo_Application::get('options')->minimumCommonWordLength;					
					
					if (strlen($var) >= $minimumCommonWordLength)
					{
						$searchWords[] = $var;
					}
				}
			}
			
			$count = count($searchWords);
			
			// only continue if we have a search word
			if ($count > 0)
			{				
				// get first search word
				$searchWord1 = $searchWords[0];
				
				// make safe for query
				$safeSearchWord1 = addslashes($searchWords[0]);
				
				if ($count > 1)
				{	
					// get second search word
					$searchWord2 = $searchWords[1];	
							
					// make safe for query
					$safeSearchWord2 = addslashes($searchWords[1]);	
				}			
			}
			
			// run query only if we have a search
			if ($safeSearchWord1 != '')
			{
				// run query in model    
				$similarThreads = $this->getModelFromCache('Andy_SimilarThreads_Model')->getThreads($safeSearchWord1,$safeSearchWord2,$currentThreadId);    
			} 
			
			// prepare $viewParams for template
			$viewParams = array(
				'similarThreads' => $similarThreads,
				'searchWord1' => $searchWord1,
				'searchWord2' => $searchWord2,
			);
			
			// send to template
			return $this->responseView('Andy_SimilarThreads_ViewPublic_SimilarThreads', 'andy_similarthreads_create_thread', $viewParams);
		}
	}
}

?>
 

AndyB

Well-known member
#5
library/Andy/SimilarThreads/ControllerPublic/Thread.php

PHP:
<?php

class Andy_SimilarThreads_ControllerPublic_Thread extends XFCP_Andy_SimilarThreads_ControllerPublic_Thread
{	
	public function actionIndex()
	{
		//########################################
		// When you call a controller function (an action), it doesn't display on execution,
		// it displays when the script is finished. By default it just returns the view object. 
		// $parent = parent::actionIndex() stores the object into the parent field
		// which is how you can access ->params, ->viewName, templateName, etc...
		// If you did return parent::actionIndex(); then you'd be displaying the parent object.
		//########################################
		
		$parent = parent::actionIndex();
	
		//########################################
		// Return parent action if this is a redirect or other non View response.
		// See /library/XenForo/ControllerResponse for different types of responses.
		//########################################
			 
		if (!$parent instanceof XenForo_ControllerResponse_View)
		{
			return parent::actionIndex();
		}
		
		// get options from Admin CP -> Options -> Similar Threads -> Show Below First Post    
		$showBelowFirstPost = XenForo_Application::get('options')->showBelowFirstPost;	
			
        // get options from Admin CP -> Options -> Similar Threads -> Show Below Quick Reply    
        $showBelowQuickReply = XenForo_Application::get('options')->showBelowQuickReply;	
	
		// show similar threads if true
		if ($showBelowFirstPost OR $showBelowQuickReply)
		{ 			
			// declare variables
			$viewParams = array();
			$searchWords = array();
			$similarThreads = array();
			$searchWord1 = '';
			$searchWord2 = '';	 
			$safeSearchWord1 = '';
			$safeSearchWord2 = '';
			
			// get $currentThreadId
			$currentThreadId = $parent->params['thread']['thread_id'];	
			
			// get $threadTitle
			$threadTitle = $parent->params['thread']['title']; 
			
			// put into array
			$threadTitle = explode(' ', $threadTitle);
			
			// get options from Admin CP -> Options -> Similar Threads -> Common Words    
			$commonWords = XenForo_Application::get('options')->commonWords;
			
			// convert to lowercase
			$commonWords = strtolower($commonWords);    
			
			// put $commonWordsLower into an array
			$commonWords = explode(' ', $commonWords); 
			
			// remove any common words from array
			foreach($threadTitle as $var)
			{
				if (!in_array(strtolower($var), $commonWords))
				{
					// get options from Admin CP -> Options -> Similar Threads -> Miniumum Common Word Length    
					$minimumCommonWordLength = XenForo_Application::get('options')->minimumCommonWordLength;					
					
					if (strlen($var) >= $minimumCommonWordLength)
					{
						$searchWords[] = $var;
					}
				}
			}
			
			$count = count($searchWords);
			
			// only continue if we have a search word
			if ($count > 0)
			{				
				// get first search word
				$searchWord1 = $searchWords[0];
				
				// make safe for query
				$safeSearchWord1 = addslashes($searchWords[0]);
				
				if ($count > 1)
				{	
					// get second search word
					$searchWord2 = $searchWords[1];	
							
					// make safe for query
					$safeSearchWord2 = addslashes($searchWords[1]);	
				}			
			}
			
			// run query only if we have a search
			if ($safeSearchWord1 != '')
			{
				// run query in model    
				$similarThreads = $this->getModelFromCache('Andy_SimilarThreads_Model')->getThreads($safeSearchWord1,$safeSearchWord2,$currentThreadId);    
			} 
			
			// prepare $viewParams for template
			$viewParams = array(
				'similarThreads' => $similarThreads,
				'showBelowFirstPost' => $showBelowFirstPost,
				'showBelowQuickReply' => $showBelowQuickReply,
				'searchWord1' => $searchWord1,
				'searchWord2' => $searchWord2,
			);
			
			return $this->responseView($parent->viewName, $parent->templateName, array_merge($parent->params, $viewParams));	
		}
		else
		{
			// neither option switch is set so return parent action
			return parent::actionIndex();	
		}
	}
}

?>
 

AndyB

Well-known member
#6
library/Andy/SimilarThreads/Listener.php

PHP:
<?php

class Andy_SimilarThreads_Listener
{
	public static function Thread($class, array &$extend)
	{
		$extend[] = 'Andy_SimilarThreads_ControllerPublic_Thread';
	}
}

?>
 

AndyB

Well-known member
#7
library/Andy/SimilarThreads/Model.php

PHP:
<?php

class Andy_SimilarThreads_Model extends XenForo_Model
{	
	public function getThreads($safeSearchWord1,$safeSearchWord2,$currentThreadId)
	{ 
		// declare variables
		$whereclause1 = '';
		$whereclause2 = '';
		$results1 = array();
		$results2 = array();
		$results3 = array();
		$excludeResults1 = '';
		$excludeResults2 = '';
		$resultsCount1 = array();
		$resultsCount2 = array();
	
		//########################################
		// $whereclause1
		// permissions
		//########################################
					
		// declare variable	
		$visitor = XenForo_Visitor::getInstance();			
		$permissionCombinationId = $visitor['permission_combination_id'];					
		
		// get permissions
		$permissions = $this->_getDb()->fetchAll("
			SELECT xf_permission_cache_content.cache_value, xf_permission_cache_content.content_id
			FROM xf_permission_cache_content
			INNER JOIN xf_forum ON xf_forum.node_id = xf_permission_cache_content.content_id
			WHERE xf_permission_cache_content.permission_combination_id = '$permissionCombinationId'
			ORDER BY xf_permission_cache_content.content_id ASC
		");	
		
		foreach ($permissions AS $k => $v)
		{
			$cache_value[] = unserialize($v['cache_value']);
			$content_id[] = $v['content_id'];				
		}
		
		$i = 0;
		
		foreach ($cache_value AS $v)
		{
			if (isset($v['view']))
			{
				$view[] = $v['view'];
			}
			
			if (isset($v['view']) AND $v['view'] != 1)
			{
				$forum_id_no_permission[] = $content_id[$i];
			}
			$i++;
		} 
		
		// exclude forums the user cannot view
		if (isset($forum_id_no_permission))
		{
			$whereclause1 = 'AND xf_thread.node_id <> ' . implode(' AND xf_thread.node_id <> ', $forum_id_no_permission);
		}
		else
		{
			$whereclause1 = '';
		}
		
		//########################################
		// $whereclause2
		// exclude thread that is being viewed
		//########################################
		
		// if coming from Thread.php don't include the thread we are viewing
		if (isset($currentThreadId))
		{
			$whereclause2 = "AND xf_thread.thread_id <> '$currentThreadId'";
		}
		
		// get option from Admin CP -> Options -> Similar Threads -> Maximum Results	
		$maxResults = XenForo_Application::get('options')->maxResults; 
		
		//########################################
		// search 1
		// $safeSearchWord1 AND $safeSearchWord2
		//########################################
		
		if ($safeSearchWord1 != '' AND $safeSearchWord2 != '')
		{
			// get threads
			$results1 = $this->_getDb()->fetchAll("
				SELECT xf_thread.thread_id, xf_thread.title, xf_thread.node_id, xf_node.title AS nodetitle, xf_thread.post_date
				FROM xf_thread
				INNER JOIN xf_node ON xf_node.node_id = xf_thread.node_id
				WHERE xf_thread.title LIKE '%$safeSearchWord1%'
				AND xf_thread.title LIKE '%$safeSearchWord2%'
				AND xf_thread.discussion_type <> 'redirect'
				$whereclause1
				$whereclause2
				ORDER BY xf_thread.thread_id DESC
				LIMIT $maxResults
			");	
			
			// prepare $results for return
			$results = $results1;
		}
		
		//########################################
		// search 2
		// $safeSearchWord1
		//########################################

		if ($safeSearchWord1 != '')
		{
		
			foreach ($results1 AS $k => $v)
			{
				$resultsCount1[] = $v['thread_id'];
				
				// exclude previously found thread_id's
				$excludeResults1 = 'AND xf_thread.thread_id <> ' . implode(' AND xf_thread.thread_id <> ', $resultsCount1);
			}
			
			$count = count($resultsCount1);
			
			if ($count < $maxResults AND is_numeric($count))
			{
				$maxResults2 = $maxResults - $count;
				
				// get threads
				$results2 = $this->_getDb()->fetchAll("
					SELECT xf_thread.thread_id, xf_thread.title, xf_thread.node_id, xf_node.title AS nodetitle, xf_thread.post_date
					FROM xf_thread
					INNER JOIN xf_node ON xf_node.node_id = xf_thread.node_id
					WHERE xf_thread.title LIKE '%$safeSearchWord1%'
					AND xf_thread.discussion_type <> 'redirect'
					$whereclause1
					$whereclause2
					$excludeResults1
					ORDER BY xf_thread.thread_id DESC
					LIMIT $maxResults2
				");	
				
				// prepare $results for return
				$results = array_merge($results1, $results2);
			}
		}
		
		//########################################
		// search 3
		// $safeSearchWord2
		//########################################
		
		if ($safeSearchWord2 != '')
		{			
			foreach ($results2 AS $k => $v)
			{
				$resultsCount2[] = $v['thread_id'];
				
				// exclude previously found thread_id's
				$excludeResults2 = 'AND xf_thread.thread_id <> ' . implode(' AND xf_thread.thread_id <> ', $resultsCount2);
			}
			
			$count = count($resultsCount1) + count($resultsCount2);
			
			if ($count < $maxResults AND is_numeric($count))
			{
				$maxResults3 = $maxResults - $count;
				
				// get threads
				$results3 = $this->_getDb()->fetchAll("
					SELECT xf_thread.thread_id, xf_thread.title, xf_thread.node_id, xf_node.title AS nodetitle, xf_thread.post_date
					FROM xf_thread
					INNER JOIN xf_node ON xf_node.node_id = xf_thread.node_id
					WHERE xf_thread.title LIKE '%$safeSearchWord2%'
					AND xf_thread.discussion_type <> 'redirect'
					$whereclause1
					$whereclause2
					$excludeResults1
					$excludeResults2
					ORDER BY xf_thread.thread_id DESC
					LIMIT $maxResults3
				");	
				
				// prepare $results for return
				$results = array_merge($results1, $results2, $results3);
			} 
		}
	
		//########################################
		// return results
		//########################################		
		
		return $results;	
	}
}

?>
 

AndyB

Well-known member
#8
library/Andy/SimilarThreads/Model/Route/Prefix/SimilarThreads.php

PHP:
<?php

class Andy_SimilarThreads_Route_Prefix_SimilarThreads implements XenForo_Route_Interface
{
	public function match($routePath, Zend_Controller_Request_Http $request, XenForo_Router $router)
	{
		return $router->getRouteMatch('Andy_SimilarThreads_ControllerPublic_Abstract', 'Index', $routePath);
	}
}

?>
 

AndyB

Well-known member
#10
Template Example. There are three templates all very similar to this one.

Code:
<xen:if is="{$similarThreads} AND {$showBelowFirstPost}">

    <div class="blueLine"></div>
    <br />

    <div class="sectionMain similarThreads">
    
        <div class="primaryContent">
        	{xen:phrase similar_threads} {$searchWord1} {$searchWord2}
        </div>
        
        <table class="dataTable">
        
        <tr class="dataRow">
        <th>{xen:phrase forum}</th>
        <th>{xen:phrase title}</th>
        <th>{xen:phrase date}</th>
        </tr>
        
        <xen:foreach loop="$similarThreads" key="$index" value="$similarThread">
        
            <tr class="dataRow">
            <td><a href="{xen:link forums, $similarThread}" title="{$similarThread.node_id}">{$similarThread.nodetitle}</a></td>
            <td><a href="{xen:link threads, $similarThread}" title="{$similarThread.title}">{$similarThread.title}</a></td>
            <td>{xen:datetime $similarThread.post_date}</td>
            </tr>
        
        </xen:foreach>
        
        </table>

    </div>
    <br />

</xen:if>
 

AndyB

Well-known member
#11
The search engine will follow these steps:
  1. Parse thread title and remove common words
  2. Select the first two search words
  3. Search for threads that contain both search words
  4. Search for threads that contain the first search word
  5. Search for threads that contain the second search word
Starting with step #3, if the maximum results set in the options is met, the remainder of queries are not run.
 

AndyB

Well-known member
#12
Other information about this add-on:
  • Five template modifications
  • Three custom templates
  • Three additional queries
  • One code event listener
  • One javascript file
  • No database changes