The following works beautifully for me. Make sure you use a regularly formatted list of sort definitions as follows:
function getSortDefinitions() { return [ 'newest' => [ [ 'created_at' => 'desc' ], [ 'id' => 'desc' ], ], 'oldest' => [ [ 'created_at' => 'asc' ], [ 'id' => 'asc' ], ] 'highest' => [ [ 'price' => 'desc' ], [ 'created_at' => 'desc' ], [ 'id' => 'desc' ], ], 'lowest' => [ [ 'price' => 'asc' ], [ 'created_at' => 'asc' ], [ 'id' => 'asc' ], ], ]; }
Aside: adding id causes the result set to have predictable ordering for records with the same timestamp. This often happens with testing instruments in which all records are stored at the same time.
Now, when someone is looking, they usually select several filters, perhaps a query and definitely a sort order. Create a table that stores this so you can create a search context to work with:
create table search_contexts ( id int primary, hash varchar(255) not null, query varchar(255) not null, filters json not null, sort varchar(255) not null, unique search_contexts_hash_uk (hash) );
To select and get a link to a search context, use the following in your language, for example:
function saveSearchContext($query, $filters, $sort) { // Assuming some magic re: JSON encoding of $filters $hash = md5(json_encode(compact('query', 'filters', 'sort'))); return SearchContext::firstOrCreate(compact('hash', 'query', 'filters', 'sort')); }
Please note that we only insert the search context if it is no longer with the same parameters. Thus, we get one unique row for each search. You can choose to be overloaded with a volume and save one per search. If you decide to do this, use uniqid instead of md5 and just create a record.
On the results index page, whenever you create a link to the details page, use the hash as the query parameter, for example:
http:
In your detail page code, do the following:
function getAdjacentDocument($search, $documentId, $next = true) { $sortDefinitions = getSortDefinitions(); if (!$next) { // Reverse the sort definitions by looping through $sortDefinitions // and swapping asc and desc around $sortDefinitions = array_map($sortDefinitions, function ($defn) { return array_map($defn, function ($array) { $field = head(array_keys($array)); $direction = $array[$field]; $direction = $direction == 'asc' ? 'desc' : 'asc'; return [ $field => $direction ]; }); }); } // Add a must_not filter which will ensure that the // current page document ID is *not* in the results. $filters['blacklist'] = $documentId; $params = [ 'body' => [ 'query' => generateQuery($search->query, $filters), 'sort' => $sortDefinitions[$sort], // We are only interested in 1 document adjacent // to this one, limit results 'size' => 1 ] ]; $response = Elasticsearch::search($params); if ($response['found']) { return $response['hits']['hits'][0]; } } function getNextDocument($search, $documentId) { return getAdjacentDocument($search, $documentId, true); } function getPreviousDocument($search, $documentId) { return getAdjacentDocument($search, $documentId, false); } // Retrieve the search context given it hash as query parameter $searchContext = SearchContext::whereHash(Input::query('search'))->first(); // From the route segment $documentId = Input::route('id'); $currentDocument = Elasticsearch::get([ 'id' => $documentId, 'index' => 'documents' ]); $previousDocument = getPreviousDocument($searchContext, $documentId); $nextDocument = getNextDocument($searchContext, $documentId);
The key to this method is that you generate two requests in addition to get for a detailed record.
One search goes forward from this record, the other backwards from this record, given the same search context in both cases, so they work according to eachother.
In both cases, you take the first record, which is not our current record, and it must be correct.