Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/contracts/src/Telescope/TelescopeTag.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ enum TelescopeTag: string
case Algolia = 'algolia';
case Meilisearch = 'meilisearch';
case Saloon = 'saloon';
case Turbopuffer = 'turbopuffer';
case Typesense = 'typesense';

/**
Expand All @@ -38,6 +39,7 @@ public function description(): string
self::Algolia => 'Scout Algolia driver.',
self::Meilisearch => 'Scout Meilisearch driver.',
self::Saloon => 'Hypervel Saloon — API integration package.',
self::Turbopuffer => 'Scout Turbopuffer driver.',
self::Typesense => 'Scout Typesense driver.',
};
}
Expand Down
2 changes: 2 additions & 0 deletions src/docs/porting-from-laravel.md
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,8 @@ Fortify ignores Laravel's `fortify.passwords` setting. Declare the password rese

Hypervel compiles integer and float values passed to Scout's Algolia `where`, `whereIn`, and `whereNotIn` methods as numeric comparisons. Numeric-looking strings remain facet values. When porting an Algolia index, ensure the indexed attribute type matches the PHP value type used by these filters.

Scout cannot generate embeddings with the Laravel AI SDK, and its database engine does not support semantic or hybrid search. Models whose `toSearchableEmbedding` method returns text must return precomputed embedding arrays or switch to the engine's native embeddings. See [semantic search](/docs/{{version}}/scout#semantic-search).

<a name="socialite"></a>
### Socialite

Expand Down
277 changes: 270 additions & 7 deletions src/docs/scout.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/docs/search.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ For semantic search that matches results by *meaning* rather than exact keywords
<a name="introduction-scout-search-engines"></a>
#### Hypervel Scout Search

For applications that want a `Searchable` trait that automatically keeps search indexes in sync with Eloquent models, [Hypervel Scout](/docs/{{version}}/scout) offers both a built-in database engine and drivers for third-party services like Algolia, Meilisearch, and Typesense.
For applications that want a `Searchable` trait that automatically keeps search indexes in sync with Eloquent models, [Hypervel Scout](/docs/{{version}}/scout) offers both a built-in database engine and drivers for third-party services like Algolia, Meilisearch, Typesense, and Turbopuffer.

<a name="full-text-search"></a>
## Full-Text Search
Expand Down
1 change: 1 addition & 0 deletions src/scout/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ Documentation: https://hypervel.org/docs/scout
- Pausing search syncing with `withoutSyncingToSearch()` or `disableSearchSyncing()` applies only to the current coroutine, so other requests keep indexing. See [Pausing Indexing](https://hypervel.org/docs/scout#pausing-indexing).
- `MeilisearchEngine::generateTenantToken()` takes the search rules, the parent key's UID, the key itself and an optional expiry. Laravel's engine forwards the call to the Meilisearch client, whose method takes the UID, the search rules and an options array. See [Tenant Tokens](https://hypervel.org/docs/scout#meilisearch-tenant-tokens).
- `scout:delete-all-indexes` refuses to run without a configured Scout prefix unless you pass `--force`.
- Scout cannot generate embeddings with the Laravel AI SDK, so semantic and hybrid search use engine-native embeddings or precomputed vectors, and the database engine does not support them. See [Semantic Search](https://hypervel.org/docs/scout#semantic-search).
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Ported from: https://github.com/laravel/scout
2 changes: 2 additions & 0 deletions src/scout/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"algolia",
"meilisearch",
"typesense",
"turbopuffer",
"database"
],
"authors": [
Expand Down Expand Up @@ -47,6 +48,7 @@
"hypervel/coroutine": "^0.4",
"hypervel/database": "^0.4",
"hypervel/foundation": "^0.4",
"hypervel/http": "^0.4",
"hypervel/macroable": "^0.4",
"hypervel/pagination": "^0.4",
"hypervel/queue": "^0.4",
Expand Down
62 changes: 61 additions & 1 deletion src/scout/config/scout.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
| using Scout. This connection is used when syncing all models to the
| search service. You should adjust this based on your needs.
|
| Supported: "algolia", "meilisearch", "typesense", "database", "collection", "null"
| Supported: "algolia", "meilisearch", "typesense", "turbopuffer",
| "database", "collection", "null"
|
*/

Expand Down Expand Up @@ -197,6 +198,21 @@
// 'users' => [
// 'filterableAttributes' => ['id', 'name', 'email'],
// 'sortableAttributes' => ['created_at'],
// 'embedders' => [
// 'default' => [
// 'source' => 'userProvided',
// 'dimensions' => 1536,
// ],
// ],
// ],
],
'model-settings' => [
// Per-model settings can be defined here:
// App\Models\User::class => [
// 'embedding' => [

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The Turbopuffer model-settings example documents only attribute + dimensions for embedding, but with the default hypervel-ai driver generateEmbeddings() unconditionally throws (AI-generated embeddings are not available in Hypervel). Following the example as written yields a runtime exception on the first index/semantic operation unless the model implements toSearchableEmbedding() returning a precomputed vector array. The comment should state that toSearchableEmbedding() must return precomputed vectors (or the driver must be turbopuffer), matching the README's documented limitation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/scout/config/scout.php, line 212:

<comment>The Turbopuffer `model-settings` example documents only `attribute` + `dimensions` for `embedding`, but with the default `hypervel-ai` driver `generateEmbeddings()` unconditionally throws (`AI-generated embeddings are not available in Hypervel`). Following the example as written yields a runtime exception on the first index/semantic operation unless the model implements `toSearchableEmbedding()` returning a precomputed vector array. The comment should state that `toSearchableEmbedding()` must return precomputed vectors (or the driver must be `turbopuffer`), matching the README's documented limitation.</comment>

<file context>
@@ -197,6 +198,21 @@
+        'model-settings' => [
+            // Per-model settings can be defined here:
+            // App\Models\User::class => [
+            //     'embedding' => [
+            //         'embedder' => 'default',
+            //         'dimensions' => 1536,
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declining. The example matches Laravel Scout's, and the same settings apply to precomputed embeddings. The Turbopuffer documentation explains that Scout doesn't generate embeddings yet and shows a model returning its precomputed embedding from toSearchableEmbedding().

// 'embedder' => 'default',
// 'dimensions' => 1536,
// ],
// ],
],
],
Expand Down Expand Up @@ -250,8 +266,52 @@
// 'search-parameters' => [
// 'query_by' => 'name',
// ],
// 'embedding' => [
// 'attribute' => 'embedding',
// 'dimensions' => 1536,
// ],
// ],
],
'import_action' => env('TYPESENSE_IMPORT_ACTION', 'upsert'),
],

/*
|--------------------------------------------------------------------------
| Turbopuffer Configuration
|--------------------------------------------------------------------------
|
| Here you may configure your Turbopuffer connection and the schema and
| searchable attributes defined by each of your application's models.
| Turbopuffer is a scalable engine with full-text and vector search.
| Timeouts are measured in seconds. Omitted region, timeout, and retry
| members use the values shown below.
|
*/

'turbopuffer' => [
'api_key' => env('TURBOPUFFER_API_KEY'),
'region' => env('TURBOPUFFER_REGION', 'gcp-us-central1'),
'base_url' => env('TURBOPUFFER_BASE_URL'),
'timeout' => (int) env('TURBOPUFFER_TIMEOUT', 60),
'connect_timeout' => (int) env('TURBOPUFFER_CONNECT_TIMEOUT', 5),
'retries' => (int) env('TURBOPUFFER_RETRIES', 3),
'model-settings' => [
// Per-model settings can be defined here:
// App\Models\User::class => [
// 'searchable-attributes' => [
// 'name' => 2,
// 'email' => 1,
// ],
// 'embedding' => [
// 'attribute' => 'embedding',
// 'dimensions' => 1536,
// ],
// 'schema' => [
// 'name' => ['type' => 'string', 'full_text_search' => true],
// 'email' => ['type' => 'string', 'full_text_search' => true],
// 'embedding' => ['type' => '[1536]f32', 'ann' => true],
// ],
// ],
],
],
];
72 changes: 72 additions & 0 deletions src/scout/src/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
use Hypervel\Scout\Contracts\PaginatesEloquentModels;
use Hypervel\Scout\Contracts\PaginatesEloquentModelsUsingDatabase;
use Hypervel\Scout\Contracts\SearchableInterface;
use Hypervel\Scout\Contracts\SupportsSemanticSearch;
use Hypervel\Scout\Engines\Engine;
use Hypervel\Scout\Exceptions\NotSupportedException;
use Hypervel\Scout\Exceptions\ScoutException;
use Hypervel\Support\Collection;
use Hypervel\Support\LazyCollection;
use Hypervel\Support\Traits\Conditionable;
Expand Down Expand Up @@ -100,6 +103,23 @@ class Builder
*/
public array $orders = [];

/**
* Indicates that the query should use semantic search.
*/
public bool $semanticSearch = false;

/**
* The minimum similarity for semantic search results.
*/
public float|int|null $minimumSimilarity = null;

/**
* The hybrid search ranking weights.
*
* @var null|array{text_weight: float|int, semantic_weight: float|int}
*/
public ?array $hybridSearch = null;

/**
* Extra options that should be applied to the search.
*
Expand Down Expand Up @@ -289,6 +309,53 @@ public function oldest(?string $column = null): static
return $this->orderBy($column, 'asc');
}

/**
* Perform a semantic search for the query expression.
*
* @return $this
*/
public function semantic(float|int|null $minSimilarity = null): static
{
if (trim($this->query) === '') {
throw new ScoutException('Semantic searches require a non-empty query.');
}

$this->semanticSearch = true;
$this->minimumSimilarity = $minSimilarity;
$this->hybridSearch = null;

return $this;
}

/**
* Perform a hybrid full-text and semantic search for the query expression.
*
* @return $this
*/
public function hybrid(
float|int $textWeight = 1,
float|int $semanticWeight = 1,
float|int|null $minSimilarity = null
): static {
if (trim($this->query) === '') {
throw new ScoutException('Hybrid searches require a non-empty query.');
}

if ($textWeight <= 0 || $semanticWeight <= 0) {
throw new ScoutException('Hybrid search weights must be positive numbers.');
}

$this->semanticSearch = false;
$this->minimumSimilarity = $minSimilarity;

$this->hybridSearch = [
'text_weight' => $textWeight,
'semantic_weight' => $semanticWeight,
];

return $this;
}

/**
* Set extra options for the search query.
*
Expand Down Expand Up @@ -620,6 +687,11 @@ protected function preparedEngine(): Engine

Scout::prepareBuilder($this, $engine);

// Check after preparation because the callback may enable semantic search.
if ($this->semanticSearch && ! $engine instanceof SupportsSemanticSearch) {
throw new NotSupportedException('The configured Scout engine does not support semantic search.');
}
Comment on lines +691 to +693

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Revalidate the query after preparation; a callback can blank it after semantic() or hybrid() succeeds, sending an empty query to the engine.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/scout/src/Builder.php, line 691:

<comment>Revalidate the query after preparation; a callback can blank it after `semantic()` or `hybrid()` succeeds, sending an empty query to the engine.</comment>

<file context>
@@ -620,6 +687,11 @@ protected function preparedEngine(): Engine
         Scout::prepareBuilder($this, $engine);
 
+        // Check after preparation because the callback may enable semantic search.
+        if ($this->semanticSearch && ! $engine instanceof SupportsSemanticSearch) {
+            throw new NotSupportedException('The configured Scout engine does not support semantic search.');
+        }
</file context>
Suggested change
if ($this->semanticSearch && ! $engine instanceof SupportsSemanticSearch) {
throw new NotSupportedException('The configured Scout engine does not support semantic search.');
}
if (($this->semanticSearch || $this->hybridSearch !== null) && trim($this->query) === '') {
throw new ScoutException('Semantic and hybrid searches require a non-empty query.');
}
if ($this->semanticSearch && ! $engine instanceof SupportsSemanticSearch) {
throw new NotSupportedException('The configured Scout engine does not support semantic search.');
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declining. This only happens if an application's own preparation callback clears the query after semantic() or hybrid() has accepted it, which isn't a supported use of that callback.


return $engine;
}

Expand Down
15 changes: 15 additions & 0 deletions src/scout/src/Contracts/SupportsSemanticSearch.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Hypervel\Scout\Contracts;

/**
* Contract for engines that support semantic and hybrid search.
*
* Semantic searches fail on other engines, while hybrid searches fall back
* to their normal full-text search.
*/
interface SupportsSemanticSearch
{
}
23 changes: 21 additions & 2 deletions src/scout/src/EngineManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
use Hypervel\Scout\Engines\Engine;
use Hypervel\Scout\Engines\MeilisearchEngine;
use Hypervel\Scout\Engines\NullEngine;
use Hypervel\Scout\Engines\TurbopufferEngine;
use Hypervel\Scout\Engines\TypesenseEngine;
use Hypervel\Scout\Services\Turbopuffer\TurbopufferClient;
use InvalidArgumentException;
use Meilisearch\Client as MeilisearchClient;
use Meilisearch\Meilisearch;
Expand Down Expand Up @@ -139,7 +141,8 @@ public function createMeilisearchDriver(): MeilisearchEngine

return new MeilisearchEngine(
$this->container->make(MeilisearchClient::class),
$config->boolean('scout.soft_delete')
$config->boolean('scout.soft_delete'),
$config->array('scout.meilisearch', [])
);
}

Expand Down Expand Up @@ -173,7 +176,8 @@ public function createTypesenseDriver(): TypesenseEngine

return new TypesenseEngine(
$this->container->make(TypesenseClient::class),
$config->integer('scout.typesense.max_total_results', 1000)
$config->integer('scout.typesense.max_total_results', 1000),
$config->array('scout.typesense', [])
);
}

Expand All @@ -193,6 +197,21 @@ protected function ensureTypesenseClientIsInstalled(): void
);
}

/**
* Create a Turbopuffer engine instance.
*/
public function createTurbopufferDriver(): TurbopufferEngine
{
/** @var Repository $config */
$config = $this->container->make('config');

return new TurbopufferEngine(
$this->container->make(TurbopufferClient::class),
$config->array('scout.turbopuffer', []),
$config->boolean('scout.soft_delete'),
);
}

/**
* Create a collection engine instance.
*/
Expand Down
Loading
Loading