-
-
Notifications
You must be signed in to change notification settings - Fork 17
Add Scout semantic and hybrid search and the Turbopuffer engine #641
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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" | ||
| | | ||
| */ | ||
|
|
||
|
|
@@ -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' => [ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: The Turbopuffer Prompt for AI agents
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| // 'embedder' => 'default', | ||
| // 'dimensions' => 1536, | ||
| // ], | ||
| // ], | ||
| ], | ||
| ], | ||
|
|
@@ -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], | ||
| // ], | ||
| // ], | ||
| ], | ||
| ], | ||
| ]; | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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; | ||||||||||||||||||||||
|
|
@@ -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. | ||||||||||||||||||||||
| * | ||||||||||||||||||||||
|
|
@@ -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. | ||||||||||||||||||||||
| * | ||||||||||||||||||||||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Revalidate the query after preparation; a callback can blank it after Prompt for AI agents
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return $engine; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| 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 | ||
| { | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.