Skip to content
This repository was archived by the owner on Apr 1, 2024. It is now read-only.

Commit 82bbdf4

Browse files
Merge pull request #3 from dark4ce/master
Add orWhereFuzzy option and query logic update
2 parents 54e5318 + 5bf847d commit 82bbdf4

5 files changed

Lines changed: 181 additions & 44 deletions

File tree

src/Macros/OrderByFuzzy.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class OrderByFuzzy
1414
public static function make(Builder $builder, $fields) : Builder
1515
{
1616
foreach ((array) $fields as $field) {
17-
$builder->orderBy('relevance_' . str_replace('.', '_', $field), 'desc');
17+
$builder->orderBy('fuzzy_relevance_' . str_replace('.', '_', $field), 'desc');
1818
}
1919

2020
return $builder;

src/Macros/WhereFuzzy.php

Lines changed: 111 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
<?php declare(strict_types = 1);
1+
<?php declare(strict_types=1);
22

33
namespace Quest\Macros;
44

5+
use Illuminate\Database\Query\Builder;
56
use Quest\Matchers\ExactMatcher;
67
use Illuminate\Support\Facades\DB;
78
use Quest\Matchers\AcronymMatcher;
89
use Quest\Matchers\InStringMatcher;
910
use Quest\Matchers\StudlyCaseMatcher;
10-
use Illuminate\Database\Query\Builder;
1111
use Quest\Matchers\StartOfWordsMatcher;
1212
use Quest\Matchers\StartOfStringMatcher;
1313
use Quest\Matchers\TimesInStringMatcher;
@@ -22,52 +22,140 @@ class WhereFuzzy
2222
*
2323
**/
2424
protected static array $matchers = [
25-
ExactMatcher::class => 100,
26-
StartOfStringMatcher::class => 50,
27-
AcronymMatcher::class => 42,
25+
ExactMatcher::class => 100,
26+
StartOfStringMatcher::class => 50,
27+
AcronymMatcher::class => 42,
2828
ConsecutiveCharactersMatcher::class => 40,
29-
StartOfWordsMatcher::class => 35,
30-
StudlyCaseMatcher::class => 32,
31-
InStringMatcher::class => 30,
32-
TimesInStringMatcher::class => 8,
29+
StartOfWordsMatcher::class => 35,
30+
StudlyCaseMatcher::class => 32,
31+
InStringMatcher::class => 30,
32+
TimesInStringMatcher::class => 8,
3333
];
3434

35+
/**
36+
* Construct a fuzzy search expression.
37+
*
38+
**/
39+
public static function make(Builder $builder, $field, $value): Builder
40+
{
41+
$value = static::escapeValue($value);
42+
$nativeField = '`' . str_replace('.', '`.`', trim($field, '` ')) . '`';
3543

44+
if (!is_array($builder->columns) || empty($builder->columns)) {
45+
$builder->columns = ['*'];
46+
}
47+
$builder
48+
->addSelect([static::pipeline($field, $nativeField, $value)])
49+
->having('fuzzy_relevance_' . str_replace('.', '_', $field), '>', 0);
50+
51+
static::calculateTotalRelevanceColumn($builder);
52+
return $builder;
53+
}
3654

3755
/**
3856
* Construct a fuzzy search expression.
3957
*
4058
**/
41-
public static function make(Builder $builder, $field, $value) : Builder
59+
public static function makeOr(Builder $builder, $field, $value): Builder
4260
{
43-
$value = str_replace(['"', "'", '`'], '', $value);
44-
45-
$native = '`' . str_replace('.', '`.`', trim($field, '` ')) . '`';
46-
$value = substr(DB::connection()->getPdo()->quote($value), 1, -1);
4761

48-
if (! is_array($builder->columns) || empty($builder->columns)) {
62+
$value = static::escapeValue($value);
63+
$nativeField = '`' . str_replace('.', '`.`', trim($field, '` ')) . '`';
64+
65+
if (!is_array($builder->columns) || empty($builder->columns)) {
4966
$builder->columns = ['*'];
5067
}
68+
$builder
69+
->addSelect([static::pipeline($field, $nativeField, $value)])
70+
->orHaving('fuzzy_relevance_' . str_replace('.', '_', $field), '>', 0);
71+
72+
static::calculateTotalRelevanceColumn($builder);
5173

52-
return $builder
53-
->addSelect(static::pipeline($field, $native, $value))
54-
->orderBy('relevance_' . str_replace('.', '_', $field), 'desc')
55-
->having('relevance_' . str_replace('.', '_', $field), '>', 0);
74+
return $builder;
5675
}
5776

77+
/**
78+
* Manage relevance columns SUM for total relevance ORDER
79+
* Searches all relevance columns and parses the relevance expressions to create the total relevance column
80+
* and creates the order statement for it
81+
* @param $builder
82+
* @return bool
83+
*/
84+
protected static function calculateTotalRelevanceColumn($builder): bool
85+
{
86+
if (!empty($builder->columns)) {
87+
$existingRelevanceColumns = [];
88+
$sumColumnIdx = null;
89+
// search for fuzzy_relevance_* columns and _fuzzy_relevance_ position
90+
foreach ($builder->columns as $as => $column) {
91+
if ($column instanceof Expression) {
92+
if (stripos($column->getValue(), 'AS fuzzy_relevance_')) {
93+
$matches = [];
94+
preg_match('/AS (fuzzy_relevance_.*)$/', $column->getValue(), $matches);
95+
if (!empty($matches[1])) {
96+
$existingRelevanceColumns[$as] = $matches[1];
97+
}
98+
} elseif (stripos($column->getValue(), 'AS _fuzzy_relevance_')) {
99+
$sumColumnIdx = $as;
100+
}
101+
}
102+
}
103+
// glue together all relevance expresions under _fuzzy_relevance_ column
104+
$relevanceTotalColumn = '';
105+
foreach ($existingRelevanceColumns as $as => $column) {
106+
$relevanceTotalColumn .= (!empty($relevanceTotalColumn) ? ' + ' : '')
107+
. '('
108+
. str_ireplace(' AS ' . $column, '', $builder->columns[$as]->getValue())
109+
. ')';
110+
}
111+
$relevanceTotalColumn .= ' AS _fuzzy_relevance_';
112+
113+
if (is_null($sumColumnIdx)) {
114+
// no sum column yet, just add this one
115+
$builder
116+
->addSelect([new Expression($relevanceTotalColumn)]);
117+
} else {
118+
// update the existing one
119+
$builder->columns[$sumColumnIdx] = new Expression($relevanceTotalColumn);
120+
}
121+
// only add the _fuzzy_relevance_ ORDER once
122+
if (
123+
!$builder->orders
124+
|| ($builder->orders
125+
&& array_search('_fuzzy_relevance_',
126+
array_column($builder->orders, 'column')
127+
) === false
128+
)
129+
) {
130+
$builder->orderBy('_fuzzy_relevance_', 'desc');
131+
}
132+
return true;
133+
}
134+
return false;
135+
}
58136

137+
/**
138+
* Escape value input for fuzzy search
139+
* @param $value
140+
* @return false|string
141+
*/
142+
protected static function escapeValue($value)
143+
{
144+
$value = str_replace(['"', "'", '`'], '', $value);
145+
$value = substr(DB::connection()->getPdo()->quote($value), 1, -1);
146+
return $value;
147+
}
59148

60149
/**
61150
* Execute each of the pattern matching classes to generate the required SQL.
62151
*
63152
**/
64-
protected static function pipeline($field, $native, $value) : Expression
153+
protected static function pipeline($field, $native, $value): Expression
65154
{
66155
$sql = collect(static::$matchers)->map(
67-
fn($multiplier, $matcher) =>
68-
(new $matcher($multiplier))->buildQueryString("COALESCE($native, '')", $value)
156+
fn($multiplier, $matcher) => (new $matcher($multiplier))->buildQueryString("COALESCE($native, '')", $value)
69157
);
70158

71-
return DB::raw($sql->implode(' + ') . ' AS relevance_' . str_replace('.', '_', $field));
159+
return DB::raw($sql->implode(' + ') . ' AS fuzzy_relevance_' . str_replace('.', '_', $field));
72160
}
73161
}

src/ServiceProvider.php

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
<?php declare(strict_types = 1);
1+
<?php declare(strict_types=1);
22

33
namespace Quest;
44

5-
use Quest\Macros\WhereFuzzy;
6-
use Quest\Macros\OrderByFuzzy;
75
use Illuminate\Database\Query\Builder;
6+
use Quest\Macros\OrderByFuzzy;
7+
use Quest\Macros\WhereFuzzy;
88
use Illuminate\Support\ServiceProvider as Provider;
99

1010
class ServiceProvider extends Provider
@@ -14,9 +14,26 @@ class ServiceProvider extends Provider
1414
* Bootstrap any application services.
1515
*
1616
**/
17-
public function boot() : void
17+
public function boot(): void
1818
{
1919
Builder::macro('orderByFuzzy', fn($fields) => OrderByFuzzy::make($this, $fields));
20-
Builder::macro('whereFuzzy', fn($field, $value) => WhereFuzzy::make($this, $field, $value));
20+
21+
Builder::macro('whereFuzzy', function ($field, $value = null) {
22+
// check if first param is a closure and execute it if it is, passing the current builder as parameter
23+
// so when $query->orWhereFuzzy, $query will be the current query builder, not a new instance
24+
if ($field instanceof \Closure) {
25+
$field($this);
26+
return $this;
27+
}
28+
// if $query->orWhereFuzzy is called in the closure, or directly by the query builder, do this
29+
return WhereFuzzy::make($this, $field, $value);
30+
});
31+
Builder::macro('orWhereFuzzy', function ($field, $value = null) {
32+
if ($field instanceof \Closure) {
33+
$field($this);
34+
return $this;
35+
}
36+
return WhereFuzzy::makeOr($this, $field, $value);
37+
});
2138
}
2239
}

support/migrations/2014_10_12_000000_create_users_table.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public function up() : void
1616
Schema::create('users', function(Blueprint $table) {
1717
$table->bigIncrements('id');
1818
$table->string('name');
19+
$table->string('nickname');
1920
$table->string('country');
2021
});
2122
}

tests/PackageTest.php

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<?php declare(strict_types = 1);
1+
<?php declare(strict_types=1);
22

33
namespace Quest\Tests;
44

@@ -13,12 +13,12 @@ class PackageTest extends TestCase
1313
* Setup the test environment.
1414
*
1515
**/
16-
protected function setUp() : void
16+
protected function setUp(): void
1717
{
1818
parent::setUp();
1919

2020
app()['config']->set('database.default', 'mysql');
21-
app()['config']->set('database.connections.mysql', [
21+
$dbSetup = [
2222
'driver' => 'mysql',
2323
'url' => env('DATABASE_URL'),
2424
'host' => env('DB_HOST', '127.0.0.1'),
@@ -33,22 +33,22 @@ protected function setUp() : void
3333
'prefix_indexes' => true,
3434
'strict' => true,
3535
'engine' => null,
36-
]);
36+
];
37+
app()['config']->set('database.connections.mysql', $dbSetup);
3738

3839
(new ServiceProvider(app()))->boot();
3940

4041
$this->loadMigrationsFrom(__DIR__ . '/../support/migrations');
4142

4243
DB::table('users')->truncate();
4344

44-
DB::table('users')->insert(['name' => 'John Doe', 'country' => 'United States']);
45-
DB::table('users')->insert(['name' => 'Jane Doe', 'country' => 'United Kingdom']);
46-
DB::table('users')->insert(['name' => 'Fred Doe', 'country' => 'France']);
47-
DB::table('users')->insert(['name' => 'William Doe', 'country' => 'Italy']);
45+
DB::table('users')->insert(['name' => 'John Doe', 'nickname' => 'jndoe', 'country' => 'United States']);
46+
DB::table('users')->insert(['name' => 'Jane Doe', 'nickname' => 'jndoe', 'country' => 'United Kingdom']);
47+
DB::table('users')->insert(['name' => 'Fred Doe', 'nickname' => 'fredrick', 'country' => 'France']);
48+
DB::table('users')->insert(['name' => 'William Doe', 'nickname' => 'willy', 'country' => 'Italy']);
4849
}
4950

5051

51-
5252
/** @test */
5353
public function it_can_perform_a_fuzzy_search_and_receive_one_result()
5454
{
@@ -61,7 +61,6 @@ public function it_can_perform_a_fuzzy_search_and_receive_one_result()
6161
}
6262

6363

64-
6564
/** @test */
6665
public function it_can_perform_a_fuzzy_search_and_receive_multiple_results()
6766
{
@@ -75,7 +74,6 @@ public function it_can_perform_a_fuzzy_search_and_receive_multiple_results()
7574
}
7675

7776

78-
7977
/** @test */
8078
public function it_can_perform_a_fuzzy_search_and_paginate_multiple_results()
8179
{
@@ -93,7 +91,6 @@ public function it_can_perform_a_fuzzy_search_and_paginate_multiple_results()
9391
}
9492

9593

96-
9794
/** @test */
9895
public function it_can_perform_a_fuzzy_search_across_multiple_fields()
9996
{
@@ -107,7 +104,6 @@ public function it_can_perform_a_fuzzy_search_across_multiple_fields()
107104
}
108105

109106

110-
111107
/** @test */
112108
public function it_can_order_a_fuzzy_search_by_one_field()
113109
{
@@ -123,7 +119,6 @@ public function it_can_order_a_fuzzy_search_by_one_field()
123119
}
124120

125121

126-
127122
/** @test */
128123
public function it_can_order_a_fuzzy_search_by_multiple_fields()
129124
{
@@ -139,7 +134,6 @@ public function it_can_order_a_fuzzy_search_by_multiple_fields()
139134
}
140135

141136

142-
143137
/** @test */
144138
public function it_can_perform_an_eloquent_fuzzy_search()
145139
{
@@ -149,4 +143,41 @@ public function it_can_perform_an_eloquent_fuzzy_search()
149143
$this->assertCount(1, $results);
150144
$this->assertEquals('Jane Doe', $results->first()->name);
151145
}
146+
147+
/** @test */
148+
public function it_can_perform_an_eloquent_fuzzy_or_search()
149+
{
150+
$results = User::whereFuzzy(function ($query) {
151+
$query->orWhereFuzzy('name', 'jndoe');
152+
$query->orWhereFuzzy('nickname', 'jndoe');
153+
})
154+
->get();
155+
156+
$this->assertEquals('John Doe', $results->first()->name);
157+
}
158+
/** @test */
159+
public function it_can_perform_an_eloquent_fuzzy_or_search_with_order()
160+
{
161+
$results = User::whereFuzzy(function ($query) {
162+
$query->orWhereFuzzy('name', 'jad');
163+
$query->orWhereFuzzy('nickname', 'jndoe');
164+
})
165+
->orderByFuzzy('name')
166+
->get();
167+
168+
$this->assertEquals('Jane Doe', $results->first()->name);
169+
}
170+
171+
/** @test */
172+
public function it_can_perform_an_eloquent_fuzzy_and_search_with_fuzzy_order()
173+
{
174+
$results = User::whereFuzzy(function ($query) {
175+
$query->whereFuzzy('name', 'jad');
176+
$query->whereFuzzy('nickname', 'jndoe');
177+
})
178+
->orderByFuzzy('name')
179+
->get();
180+
181+
$this->assertEquals('Jane Doe', $results->first()->name);
182+
}
152183
}

0 commit comments

Comments
 (0)