Skip to content

Commit 6bd4009

Browse files
authored
Support for ordering by relevance (#27)
1 parent 123fcbe commit 6bd4009

File tree

4 files changed

+96
-11
lines changed

4 files changed

+96
-11
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
All notable changes to `laravel-cross-eloquent-search` will be documented in this file
44

5+
## 2.2.0 - 2021-09-17
6+
7+
- Support for ordering by relevance
8+
59
## 2.1.0 - 2021-08-09
610

711
- Support for Table prefixes

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Hey! We've built a Docker-based deployment tool to launch apps and sites fully c
2323
* Search through one or more [Eloquent models](https://laravel.com/docs/master/eloquent).
2424
* Support for cross-model [pagination](https://laravel.com/docs/master/pagination#introduction).
2525
* Search through single or multiple columns.
26+
* Order by (cross-model) columns or by relevance.
2627
* Use [constraints](https://laravel.com/docs/master/eloquent#retrieving-models) and [scoped queries](https://laravel.com/docs/master/eloquent#query-scopes).
2728
* [Eager load relationships](https://laravel.com/docs/master/eloquent-relationships#eager-loading) for each model.
2829
* In-database [sorting](https://laravel.com/docs/master/queries#ordering-grouping-limit-and-offset) of the combined result.
@@ -141,12 +142,26 @@ Search::add(Post::class, 'title')
141142
If you want to sort the results by another column, you can pass that column to the `add` method as a third parameter. Call the `orderByDesc` method to sort the results in descending order.
142143

143144
```php
144-
Search::add(Post::class, 'title', 'publihed_at')
145+
Search::add(Post::class, 'title', 'published_at')
145146
->add(Video::class, 'title', 'released_at')
146147
->orderByDesc()
147148
->get('learn');
148149
```
149150

151+
You can call the `orderByRelevance` method to sort the results by the number of occurrences of the search terms. Imagine these two sentences:
152+
153+
* Apple introduces iPhone 13 and iPhone 13 mini
154+
* Apple unveils new iPad mini with breakthrough performance in stunning new design
155+
156+
If you search for *Apple iPad*, the second sentence will come up first, as there are more matches of the search terms.
157+
158+
```php
159+
Search::add(Post::class, 'title')
160+
->beginWithWildcard()
161+
->orderByRelevance()
162+
->get('Apple iPad');
163+
```
164+
150165
### Pagination
151166

152167
We highly recommend paginating your results. Call the `paginate` method before the `get` method, and you'll get an instance of `\Illuminate\Contracts\Pagination\LengthAwarePaginator` as a result. The `paginate` method takes three (optional) parameters to customize the paginator. These arguments are [the same](https://laravel.com/docs/master/pagination#introduction) as Laravel's database paginator.

src/Searcher.php

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ class Searcher
4747
*/
4848
protected Collection $terms;
4949

50+
/**
51+
* Collection of search terms.
52+
*/
53+
protected Collection $termsWithoutWildcards;
54+
5055
/**
5156
* The number of items to be shown per page.
5257
*/
@@ -108,6 +113,18 @@ public function orderByDesc(): self
108113
return $this;
109114
}
110115

116+
/**
117+
* Sort the results in relevance order.
118+
*
119+
* @return self
120+
*/
121+
public function orderByRelevance(): self
122+
{
123+
$this->orderByDirection = 'relevance';
124+
125+
return $this;
126+
}
127+
111128
/**
112129
* Disable the parsing of the search term.
113130
*/
@@ -291,17 +308,17 @@ protected function initializeTerms(string $terms): self
291308
{
292309
$terms = $this->parseTerm ? $this->parseTerms($terms) : $terms;
293310

294-
$this->terms = Collection::wrap($terms)
295-
->filter()
296-
->unless($this->soundsLike, function ($terms) {
297-
return $terms->map(function ($term) {
298-
return implode([
299-
$this->beginWithWildcard ? '%' : '',
300-
$term,
301-
$this->endWithWildcard ? '%' : '',
302-
]);
303-
});
311+
$this->termsWithoutWildcards = Collection::wrap($terms)->filter();
312+
313+
$this->terms = Collection::make($this->termsWithoutWildcards)->unless($this->soundsLike, function ($terms) {
314+
return $terms->map(function ($term) {
315+
return implode([
316+
$this->beginWithWildcard ? '%' : '',
317+
$term,
318+
$this->endWithWildcard ? '%' : '',
319+
]);
304320
});
321+
});
305322

306323
return $this;
307324
}
@@ -324,6 +341,34 @@ public function addSearchQueryToBuilder(Builder $builder, ModelToSearchThrough $
324341
});
325342
}
326343

344+
/**
345+
* Adds a word count so we can order by relevance.
346+
*
347+
* @param \Illuminate\Database\Eloquent\Builder $builder
348+
* @param \ProtoneMedia\LaravelCrossEloquentSearch\ModelToSearchThrough $modelToSearchThrough
349+
* @return void
350+
*/
351+
private function addRelevanceQueryToBuilder($builder, $modelToSearchThrough)
352+
{
353+
if ($this->orderByDirection !== 'relevance') {
354+
return;
355+
}
356+
357+
$expressionsAndBindings = $modelToSearchThrough->getQualifiedColumns()->flatMap(function ($field) use ($builder) {
358+
return $this->termsWithoutWildcards->map(function ($term) use ($field) {
359+
return [
360+
'expression' => "COALESCE(CHAR_LENGTH(LOWER({$field})) - CHAR_LENGTH(REPLACE(LOWER({$field}), ?, ?)), 0)",
361+
'bindings' => [strtolower($term), substr(strtolower($term), 1)],
362+
];
363+
});
364+
});
365+
366+
$selects = $expressionsAndBindings->map->expression->implode(' + ');
367+
$bindings = $expressionsAndBindings->flatMap->bindings->all();
368+
369+
$builder->selectRaw("{$selects} as terms_count", $bindings);
370+
}
371+
327372
/**
328373
* Builds an array with all qualified columns for
329374
* both the ids and ordering.
@@ -375,6 +420,7 @@ protected function buildQueries(): Collection
375420
->select($this->makeSelects($modelToSearchThrough))
376421
->tap(function ($builder) use ($modelToSearchThrough) {
377422
$this->addSearchQueryToBuilder($builder, $modelToSearchThrough);
423+
$this->addRelevanceQueryToBuilder($builder, $modelToSearchThrough);
378424
});
379425
});
380426
}
@@ -395,6 +441,10 @@ protected function getCompiledQueryBuilder(): QueryBuilder
395441
// union the other queries together
396442
$queries->each(fn (Builder $query) => $firstQuery->union($query));
397443

444+
if ($this->orderByDirection === 'relevance') {
445+
return $firstQuery->orderBy('terms_count', 'desc');
446+
}
447+
398448
// sort by the given columns and direction
399449
return $firstQuery->orderBy(DB::raw($this->makeOrderBy()), $this->orderByDirection);
400450
}

tests/SearchTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,22 @@ public function it_can_eager_load_relations()
308308
$this->assertTrue($results->first()->relationLoaded('comments'));
309309
}
310310

311+
/** @test */
312+
public function it_can_sort_by_word_occurrence()
313+
{
314+
$videoA = Video::create(['title' => 'Apple introduces', 'subtitle' => 'iPhone 13 and iPhone 13 mini']);
315+
$videoB = Video::create(['title' => 'Apple unveils', 'subtitle' => 'new iPad mini with breakthrough performance in stunning new design']);
316+
317+
$results = Search::new()
318+
->add(Video::class, ['title', 'subtitle'])
319+
->beginWithWildcard()
320+
->orderByRelevance()
321+
->get('Apple iPad');
322+
323+
$this->assertCount(2, $results);
324+
$this->assertTrue($results->first()->is($videoB));
325+
}
326+
311327
/** @test */
312328
public function it_uses_length_aware_paginator_by_default()
313329
{

0 commit comments

Comments
 (0)