Combining keyword and proximity searches to create a classified ads site in ExpressionEngine

Posted in Web Design on Wednesday, 5th December 2012 at 2:09PM

Combining keyword and proximity searches to create a classified ads site in ExpressionEngine

UPDATE 25/02/13: I have since re-worked this site to only use the proximity search.  I have a new site on the horizon which may well use a similar setup and I will update the post if it comes to pass.

I have been working on a personal project recently called newofficeasia.com that requires search results to be sorted by distance from a user's location.  Luckily there's a great add-on called Geofinder that does just that. 

Things became a little more complictaed when I needed to add other search criteria into the mix such as keywords and other custom field data. Geofinder only selects and sorts entries based on their location but I needed the functionality of a fully fledged search module in combination with distance searching.  This technique would be ideal for a classified ads site where botht he item description and location need to be searched. Here's a page on newofficeasia.com that demonstrates what I came up with, showing offices within 20 miles of Raffles Square, Singapore that contain the keyword 'ISDN'.

Combining the likes of Solspace's Super Search and Geofinder into one Super Duper Search add-on is frankly beyond the limits of my expertise and development timeframe so I decided to give it a shot using Ajax to intercept the Super Search results and filter/sort them by proximity.  For the sake of simplicity I will exclude the Ajax aspect of my implementation from this article as it gets complicated and is unnecessary for the basic search / proximity combo to work.

To get this set up I needed a template to hold the search form (site/search_form) another template to do the calculations and provide a list of entry ids (site/search_results) and a third template which was embeded in site/search_results to display the results (site/entry_data).

Here is a simplified version of the search form including fields for the user's location, search radius and relevant keywords.


<form id="officeSearch" action="/site/search_results" method="post">
<label for="location">Location</label>
<input type="text" name="location" id="location" value="">
<label for="distance">Search radius</label>
<input type="text" name="distance" id="distance"  value="">
<label for="keywords">Keywords</label>
<input type="text" name="keywords" id="keywords"  value="">
</form>

Upon submission the user is taken to the second template with the form inputs being posted in the URL as a querystring eg. www.yoursite.com/site/search_results?location=gl205ng&distance=5&keywords=office.

The site/search_results template must have PHP enabled on output so that we can take the Super Search results and continue to manipulate them with PHP.  Here is the site/search_results template broken down.

1. I first used the Geotagger add-on to get the longitude and latitude of the user's location and pass them to PHP variables.  I used the Mo' Variables add-on to get the query string values for the ExpressionEngine parts of the template ({embed:get:location}) and $_GET['variable'] for the PHP parts.


{exp:geofinder:geocode parse="inward" geoquery="{embed:get:location}"}
<?php 
$home_lat = '{latitude}';
$home_long = '{longitude}';  
?>
{/exp:geofinder:geocode}

2.  I calculated the distances between user and office using a simple PHP function called 'find_distance' which was added to the template next.


<?php 
function find_distance($lat1,$long1,$lat2,$long2)
{
	$a = 69.1 * ($lat1-$lat2);
	$b = 53 * ($long1-$long2);

	$c = sqrt(($a*$a)+($b*$b));
     
	return round($c, 1); 
};
?>

3. Next I needed to get the Super Search results and pass them into an array ($all_offices). Super Search automatically inspects the querystring and only outputs entries that match the search criteria, in this case keywords, so that bit was taken care of. 

In order to filter and search by distance I created a variable within the Super Search tag pair ($office_dist) to hold the distance each office is from the user.  This was set using the 'find_distance' function we created in stage 2.  The {latitude} and {longitude} tags are custom fields in the office channel which need to be entered for each entry or generated by the Geocoder fieldtype.

I then checked that the distance for each entry was within the radius set in the querystring distance parameter, and if it was, added it's entry id and distance to the $all_offices array.


<?php $all_offices = array(); ?>

{exp:super_search:results
					orderby="date"
					status="Open"
					channel="offices"
}

<?php

$office_dist = find_distance("{latitude}","{longitude}",$home_lat,$home_long);

if($office_dist <= $_GET['distance'])
{
	array_push($all_offices, array(
			"entry_id" => "{entry_id}",
			"office_dist" => $office_dist));

};
?>
{/exp:super_search:results}

4. This resulted in an array holding the entry id's of all entries that matched my keywords and that fell within the radius of the location as specified by the user.  Next I needed to sort this array by distance using another short function.


<?php 

function sortByOneKey(array $array, $key, $asc = true) {
	$result = array();
	$values = array();

	foreach ($array as $id => $value) {
		$values[$id] = (float)$value[$key];
	}

	if ($asc) {
		asort($values);
	}
	else {
		arsort($values);
	}

	foreach ($values as $key => $value) {
		$result[$key] = $array[$key];
	}

	return $result;
};

$all_offices = sortByOneKey($all_offices, 'office_dist'); 

?>

5. This left me with an array of office id's and distances sorted by distance from the user.  The next step was to convert this into a variable holding just the entry id's in a format ExpressionEngine could understand.


$entry_ids = array();

foreach ($all_offices as $index => $val) {
		array_push($entry_ids, $val['entry_id']); 
};

$ids_for_ee = implode("|", $entry_ids); 

6. Finally I used a template embed to pass this string of id's to my site/entry_data template which contained the channel entries tag to output the results.

{embed="site/entry_data" advertIDS="<?php echo $ids_for_ee;?>"}

7.  That template might look a little like this in it's most basic form.


<html>
<body>

<div id="searchResults">
{exp:channel:entries channel="offices"  dynamic="no"  fixed_order="{embed:advertIDS}" status="Open" }
<article>

<h3><a href="/details/{url_title}">{title}</a></h3>

{body}

</article>
{/exp:channel:entries}
</div>

</body>
</html>

This example is a vast simplification of how I actually implemented the technique on newofficeasia.com.  The following link shows an example of a search results page produced using this method.

Example results

As you will probably notice, this example is Ajax driven so the site/search_results template is being injected into the same page as the search form.  I have also created a hashtag variable which represents the search query so that search result pages can be bookmarked and navigated backwards and forwards.  The PHP template has also been embellished somewhat to allow for pagination and alternate ordering.

For pages where search criteria other than location and distance aren't needed I have just used Geofinder to output the results.  For example, this page just needs to display serviced offices near Mongkok, Hong Kong - no keyword search needed.

Geofinder results

Add your comment

Remember me?

Notify me of follow-up comments?

Awesome man. I am just moving in to using expressionengine from previously having used Joomla and then predominantly Wordpress. This is the exact kind of idea I’m looking at for a new project. I was thinking along the same lines as you, but as I’m yet to play with expressionengine I couldn’t have imagined the beautiful simplicity of such a robust solution. Thanks a lot for this gem. Really appreciate you sharing your experience. Great job!

Posted by Alex on Thursday, 30th January 2014 at 1:05AM

Michael's Paintings

The High Sierra

The High Sierra
46 x 46 cm
Acrylics, ink and cotton on canvas

Michael's photos

Kisoro School

Kisoro School
Uganda

Featured Web Project

Ade Gascoyne Woodwork - Home

Project: Ade Gascoyne Woodworks