Skip to content

Commit 7c35ea4

Browse files
authored
Add vector query support to the query builder (#346)
* Initial support for mariadb vector similarity * Initial support for mysql vector similarity * similarity -> distance * Cleaning up * Added vector distance support to postgres compiler * Add support for vectors from subqueries * Update Mako.php * Cleaning up * Add support for sorting by vector distance * Made it easier to insert and retrieve vectors * Made it easier to retrieve vector distances * Cleaning up * Update core.php * Update CHANGELOG.md
1 parent 8a8572c commit 7c35ea4

19 files changed

Lines changed: 1408 additions & 28 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
### 12.1.0 <small>(2026-??-??)</small>
2+
3+
#### New
4+
5+
* Added vector query support to the query builder for the following databases:
6+
- `MariaDB`
7+
- `MySQL`
8+
- `Postgres`
9+
* Now possible to define custom input/output value objects for the query builder.
10+
11+
--------------------------------------------------------
12+
113

214
### 11.4.6, 12.0.2 <small>(2026-01-08)</small>
315

src/mako/Mako.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ final class Mako
1515
/**
1616
* Mako version.
1717
*/
18-
public const string VERSION = '12.0.2';
18+
public const string VERSION = '12.1.0';
1919

2020
/**
2121
* Mako major version.
@@ -25,10 +25,10 @@ final class Mako
2525
/**
2626
* Mako minor version.
2727
*/
28-
public const int VERSION_MINOR = 0;
28+
public const int VERSION_MINOR = 1;
2929

3030
/**
3131
* Mako patch version.
3232
*/
33-
public const int VERSION_PATCH = 2;
33+
public const int VERSION_PATCH = 0;
3434
}

src/mako/application/cli/commands/app/preloader/core.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@
7878
mako\database\query\Result::class,
7979
mako\database\query\ResultSet::class,
8080
mako\database\query\Subquery::class,
81+
mako\database\query\values\in\Vector::class,
82+
mako\database\query\values\out\Value::class,
83+
mako\database\query\values\out\ValueWithAliasInterface::class,
84+
mako\database\query\values\out\Vector::class,
85+
mako\database\query\values\out\VectorDistance::class,
86+
mako\database\query\values\ValueInterface::class,
87+
mako\database\query\VectorDistance::class,
8188
mako\error\ErrorHandler::class,
8289
mako\file\FileSystem::class,
8390
mako\file\Permission::class,

src/mako/database/query/Query.php

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,16 @@ public function where(array|Closure|Raw|string $column, ?string $operator = null
545545
return $this;
546546
}
547547

548+
/**
549+
* Adds a OR WHERE clause.
550+
*
551+
* @return $this
552+
*/
553+
public function orWhere(array|Closure|Raw|string $column, ?string $operator = null, mixed $value = null): static
554+
{
555+
return $this->where($column, $operator, $value, 'OR');
556+
}
557+
548558
/**
549559
* Adds a raw WHERE clause.
550560
*
@@ -565,16 +575,6 @@ public function whereRaw(array|Raw|string $column, null|array|string $operator =
565575
return $this->where($column, $operator, new Raw($raw), $separator);
566576
}
567577

568-
/**
569-
* Adds a OR WHERE clause.
570-
*
571-
* @return $this
572-
*/
573-
public function orWhere(array|Closure|Raw|string $column, ?string $operator = null, mixed $value = null): static
574-
{
575-
return $this->where($column, $operator, $value, 'OR');
576-
}
577-
578578
/**
579579
* Adds a raw OR WHERE clause.
580580
*
@@ -642,6 +642,35 @@ public function orWhereColumn(array|string $column1, string $operator, array|str
642642
return $this->whereColumn($column1, $operator, $column2, 'OR');
643643
}
644644

645+
/**
646+
* Adds a vector distance clause.
647+
*
648+
* @return $this
649+
*/
650+
public function whereVectorDistance(string $column, array|string|Subquery $vector, float $maxDistance = 0.2, VectorDistance $vectorDistance = VectorDistance::COSINE, string $separator = 'AND'): static
651+
{
652+
$this->wheres[] = [
653+
'type' => 'whereVectorDistance',
654+
'column' => $column,
655+
'vector' => $vector,
656+
'maxDistance' => $maxDistance,
657+
'vectorDistance' => $vectorDistance,
658+
'separator' => $separator,
659+
];
660+
661+
return $this;
662+
}
663+
664+
/**
665+
* Adds a vector distance clause.
666+
*
667+
* @return $this
668+
*/
669+
public function orWhereVectorDistance(string $column, array|string|Subquery $vector, float $maxDistance = 0.2, VectorDistance $vectorDistance = VectorDistance::COSINE): static
670+
{
671+
return $this->whereVectorDistance($column, $vector, $maxDistance, $vectorDistance, 'OR');
672+
}
673+
645674
/**
646675
* Adds a BETWEEN clause.
647676
*
@@ -1040,6 +1069,7 @@ public function orHavingRaw(string $raw, string $operator, mixed $value): static
10401069
public function orderBy(array|Raw|string $columns, string $order = 'ASC'): static
10411070
{
10421071
$this->orderings[] = [
1072+
'type' => 'basicOrdering',
10431073
'column' => is_array($columns) ? $columns : [$columns],
10441074
'order' => ($order === 'ASC' || $order === 'asc') ? 'ASC' : 'DESC',
10451075
];
@@ -1097,6 +1127,38 @@ public function descendingRaw(string $raw, array $parameters = []): static
10971127
return $this->orderByRaw($raw, $parameters, 'DESC');
10981128
}
10991129

1130+
/**
1131+
* Adds a vector ORDER BY clause.
1132+
*/
1133+
public function orderByVectorDistance(string $column, array|string|Subquery $vector, VectorDistance $vectorDistance = VectorDistance::COSINE, string $order = 'ASC'): static
1134+
{
1135+
$this->orderings[] = [
1136+
'type' => 'vectorDistanceOrdering',
1137+
'column' => $column,
1138+
'vector' => $vector,
1139+
'vectorDistance' => $vectorDistance,
1140+
'order' => ($order === 'ASC' || $order === 'asc') ? 'ASC' : 'DESC',
1141+
];
1142+
1143+
return $this;
1144+
}
1145+
1146+
/**
1147+
* Adds a ascending vector ORDER BY clause.
1148+
*/
1149+
public function ascendingVectorDistance(string $column, array|string|Subquery $vector, VectorDistance $vectorDistance = VectorDistance::COSINE): static
1150+
{
1151+
return $this->orderByVectorDistance($column, $vector, $vectorDistance, 'ASC');
1152+
}
1153+
1154+
/**
1155+
* Adds a descending vector ORDER BY clause.
1156+
*/
1157+
public function descendingVectorDistance(string $column, array|string|Subquery $vector, VectorDistance $vectorDistance = VectorDistance::COSINE): static
1158+
{
1159+
return $this->orderByVectorDistance($column, $vector, $vectorDistance, 'DESC');
1160+
}
1161+
11001162
/**
11011163
* Clears the ordering clauses.
11021164
*
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
/**
4+
* @copyright Frederic G. Østby
5+
* @license http://www.makoframework.com/license
6+
*/
7+
8+
namespace mako\database\query;
9+
10+
/**
11+
* Vector distance.
12+
*/
13+
enum VectorDistance
14+
{
15+
case COSINE;
16+
case EUCLIDEAN;
17+
}

src/mako/database/query/compilers/Compiler.php

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
use mako\database\query\Query;
1414
use mako\database\query\Raw;
1515
use mako\database\query\Subquery;
16+
use mako\database\query\values\out\ValueWithAliasInterface;
17+
use mako\database\query\values\ValueInterface;
1618

1719
use function array_keys;
1820
use function array_shift;
@@ -83,6 +85,28 @@ protected function raw(Raw $raw): string
8385
return $raw->getSql();
8486
}
8587

88+
/**
89+
* Compiles a value.
90+
*/
91+
protected function value(ValueInterface $value): string
92+
{
93+
$parameters = $value->getParameters();
94+
95+
if (!empty($parameters)) {
96+
$this->params = [...$this->params, ...$parameters];
97+
}
98+
99+
$sql = $value->getSql($this);
100+
101+
if ($value instanceof ValueWithAliasInterface) {
102+
if (($alias = $value->getAlias()) !== null) {
103+
$sql .= " AS {$this->escapeIdentifier($alias)}";
104+
}
105+
}
106+
107+
return $sql;
108+
}
109+
86110
/**
87111
* Compiles a subselect and merges the parameters.
88112
*/
@@ -210,11 +234,13 @@ public function escapeTableNames(array $tables): string
210234
*/
211235
public function table(Raw|string|Subquery $table): string
212236
{
213-
if ($table instanceof Raw) {
214-
return $this->raw($table);
215-
}
216-
elseif ($table instanceof Subquery) {
217-
return $this->subquery($table);
237+
if (is_object($table)) {
238+
if ($table instanceof Raw) {
239+
return $this->raw($table);
240+
}
241+
elseif ($table instanceof Subquery) {
242+
return $this->subquery($table);
243+
}
218244
}
219245

220246
return $this->escapeTableNameWithAlias($table);
@@ -286,15 +312,21 @@ public function columnNames(array $columns): string
286312
/**
287313
* Compiles a column.
288314
*/
289-
public function column(Raw|string|Subquery $column, bool $allowAlias = false): string
315+
public function column(Raw|string|Subquery|ValueInterface $column, bool $allowAlias = false): string
290316
{
291-
if ($column instanceof Raw) {
292-
return $this->raw($column);
293-
}
294-
elseif ($column instanceof Subquery) {
295-
return $this->subquery($column);
317+
if (is_object($column)) {
318+
if ($column instanceof Raw) {
319+
return $this->raw($column);
320+
}
321+
elseif ($column instanceof Subquery) {
322+
return $this->subquery($column);
323+
}
324+
elseif ($column instanceof ValueInterface) {
325+
return $this->value($column);
326+
}
296327
}
297-
elseif ($allowAlias && stripos($column, ' AS ') !== false) {
328+
329+
if ($allowAlias && stripos($column, ' AS ') !== false) {
298330
[$column, , $alias] = explode(' ', $column, 3);
299331

300332
return "{$this->columnName($column)} AS {$this->columnName($alias)}";
@@ -378,6 +410,9 @@ protected function param(mixed $param, bool $enclose = true): string
378410

379411
return '?';
380412
}
413+
elseif ($param instanceof ValueInterface) {
414+
return $this->value($param);
415+
}
381416
}
382417

383418
$this->params[] = $param;
@@ -483,6 +518,14 @@ protected function whereColumn(array $where): string
483518
return "{$this->columnName($where['column1'])} {$where['operator']} {$this->columnName($where['column2'])}";
484519
}
485520

521+
/**
522+
* Compiles vector distance clauses.
523+
*/
524+
protected function whereVectorDistance(array $where): string
525+
{
526+
throw new DatabaseException(sprintf('The [ %s ] query compiler does not support vector distance calculations.', static::class));
527+
}
528+
486529
/**
487530
* Compiles BETWEEN clauses.
488531
*/
@@ -631,6 +674,22 @@ protected function groupings(array $groupings): string
631674
return empty($groupings) ? '' : " GROUP BY {$this->columns($groupings)}";
632675
}
633676

677+
/**
678+
* Compiles a basic ordering clause.
679+
*/
680+
protected function basicOrdering(array $order): string
681+
{
682+
return "{$this->columns($order['column'])} {$order['order']}";
683+
}
684+
685+
/**
686+
* Compiles vector distance ordering clause.
687+
*/
688+
protected function vectorDistanceOrdering(array $order): string
689+
{
690+
throw new DatabaseException(sprintf('The [ %s ] query compiler does not support vector distance calculations.', static::class));
691+
}
692+
634693
/**
635694
* Compiles ORDER BY clauses.
636695
*/
@@ -643,7 +702,7 @@ protected function orderings(array $orderings): string
643702
$sql = [];
644703

645704
foreach ($orderings as $order) {
646-
$sql[] = "{$this->columns($order['column'])} {$order['order']}";
705+
$sql[] = $this->{$order['type']}($order);
647706
}
648707

649708
return ' ORDER BY ' . implode(', ', $sql);

src/mako/database/query/compilers/MariaDB.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,62 @@
77

88
namespace mako\database\query\compilers;
99

10+
use mako\database\query\Subquery;
11+
use mako\database\query\VectorDistance;
1012
use Override;
1113

14+
use function is_array;
15+
use function json_encode;
16+
1217
/**
1318
* Compiles MariaDB queries.
1419
*/
1520
class MariaDB extends MySQL
1621
{
22+
/**
23+
* Returns a vector distance calculation.
24+
*/
25+
protected function vectorDistance(array $vectorDistance): string
26+
{
27+
$vector = $vectorDistance['vector'];
28+
29+
if ($vector instanceof Subquery) {
30+
$vector = $this->subquery($vector);
31+
}
32+
else {
33+
if (is_array($vector)) {
34+
$vector = json_encode($vector);
35+
}
36+
37+
$vector = "VEC_FromText({$this->param($vector)})";
38+
}
39+
40+
$function = match ($vectorDistance['vectorDistance']) {
41+
VectorDistance::COSINE => 'VEC_DISTANCE_COSINE',
42+
VectorDistance::EUCLIDEAN => 'VEC_DISTANCE_EUCLIDEAN',
43+
};
44+
45+
return "{$function}({$this->column($vectorDistance['column'], false)}, {$vector})";
46+
}
47+
48+
/**
49+
* {@inheritDoc}
50+
*/
51+
#[Override]
52+
protected function whereVectorDistance(array $where): string
53+
{
54+
return "{$this->vectorDistance($where)} <= {$this->param($where['maxDistance'])}";
55+
}
56+
57+
/**
58+
* {@inheritDoc}
59+
*/
60+
#[Override]
61+
protected function vectorDistanceOrdering(array $order): string
62+
{
63+
return "{$this->vectorDistance($order)} {$order['order']}";
64+
}
65+
1766
/**
1867
* {@inheritDoc}
1968
*/

0 commit comments

Comments
 (0)