From a023bed41d0219d08d7dbce52948e3e5c3528381 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 25 Mar 2025 19:38:32 +0000 Subject: [PATCH] Vectors: Added command to regenerate for all Also made models configurable. Tested system scales via 86k vector entries. --- app/Config/services.php | 2 + .../Commands/RegenerateVectorsCommand.php | 46 +++++++++++++++++++ .../Services/OpenAiVectorQueryService.php | 23 +++++++--- .../Vectors/VectorQueryServiceProvider.php | 4 +- app/Search/Vectors/VectorSearchRunner.php | 1 + ..._24_155748_create_search_vectors_table.php | 2 + 6 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 app/Console/Commands/RegenerateVectorsCommand.php diff --git a/app/Config/services.php b/app/Config/services.php index a34b243f0..aafe0bacc 100644 --- a/app/Config/services.php +++ b/app/Config/services.php @@ -30,6 +30,8 @@ return [ 'openai' => [ 'endpoint' => env('OPENAI_ENDPOINT', 'https://api.openai.com'), 'key' => env('OPENAI_KEY', ''), + 'embedding_model' => env('OPENAI_EMBEDDING_MODEL', 'text-embedding-3-small'), + 'query_model' => env('OPENAI_QUERY_MODEL', 'gpt-4o'), ], 'github' => [ diff --git a/app/Console/Commands/RegenerateVectorsCommand.php b/app/Console/Commands/RegenerateVectorsCommand.php new file mode 100644 index 000000000..700d05300 --- /dev/null +++ b/app/Console/Commands/RegenerateVectorsCommand.php @@ -0,0 +1,46 @@ +delete(); + + $types = $entityProvider->all(); + foreach ($types as $type => $typeInstance) { + $this->info("Creating jobs to store vectors for {$type} data..."); + /** @var Entity[] $entities */ + $typeInstance->newQuery()->chunkById(100, function ($entities) { + foreach ($entities as $entity) { + dispatch(new StoreEntityVectorsJob($entity)); + } + }); + } + } +} diff --git a/app/Search/Vectors/Services/OpenAiVectorQueryService.php b/app/Search/Vectors/Services/OpenAiVectorQueryService.php index e0e145f3a..fea4d5c14 100644 --- a/app/Search/Vectors/Services/OpenAiVectorQueryService.php +++ b/app/Search/Vectors/Services/OpenAiVectorQueryService.php @@ -6,17 +6,26 @@ use BookStack\Http\HttpRequestService; class OpenAiVectorQueryService implements VectorQueryService { + protected string $key; + protected string $endpoint; + protected string $embeddingModel; + protected string $queryModel; + public function __construct( - protected string $endpoint, - protected string $key, + protected array $options, protected HttpRequestService $http, ) { + // TODO - Some kind of validation of options + $this->key = $this->options['key'] ?? ''; + $this->endpoint = $this->options['endpoint'] ?? ''; + $this->embeddingModel = $this->options['embedding_model'] ?? ''; + $this->queryModel = $this->options['query_model'] ?? ''; } protected function jsonRequest(string $method, string $uri, array $data): array { $fullUrl = rtrim($this->endpoint, '/') . '/' . ltrim($uri, '/'); - $client = $this->http->buildClient(10); + $client = $this->http->buildClient(30); $request = $this->http->jsonRequest($method, $fullUrl, $data) ->withHeader('Authorization', 'Bearer ' . $this->key); @@ -28,7 +37,7 @@ class OpenAiVectorQueryService implements VectorQueryService { $response = $this->jsonRequest('POST', 'v1/embeddings', [ 'input' => $text, - 'model' => 'text-embedding-3-small', + 'model' => $this->embeddingModel, ]); return $response['data'][0]['embedding']; @@ -39,15 +48,15 @@ class OpenAiVectorQueryService implements VectorQueryService $formattedContext = implode("\n", $context); $response = $this->jsonRequest('POST', 'v1/chat/completions', [ - 'model' => 'gpt-4o', + 'model' => $this->queryModel, 'messages' => [ [ 'role' => 'developer', - 'content' => 'You are a helpful assistant providing search query responses. Be specific, factual and to-the-point in response.' + 'content' => 'You are a helpful assistant providing search query responses. Be specific, factual and to-the-point in response. Don\'t try to converse or continue the conversation.' ], [ 'role' => 'user', - 'content' => "Provide a response to the below given QUERY using the below given CONTEXT\nQUERY: {$input}\n\nCONTEXT: {$formattedContext}", + 'content' => "Provide a response to the below given QUERY using the below given CONTEXT. The CONTEXT is split into parts via lines. Ignore any nonsensical lines of CONTEXT.\nQUERY: {$input}\n\nCONTEXT: {$formattedContext}", ] ], ]); diff --git a/app/Search/Vectors/VectorQueryServiceProvider.php b/app/Search/Vectors/VectorQueryServiceProvider.php index c700307e1..eae7149d0 100644 --- a/app/Search/Vectors/VectorQueryServiceProvider.php +++ b/app/Search/Vectors/VectorQueryServiceProvider.php @@ -18,9 +18,7 @@ class VectorQueryServiceProvider $service = $this->getServiceName(); if ($service === 'openai') { - $key = config('services.openai.key'); - $endpoint = config('services.openai.endpoint'); - return new OpenAiVectorQueryService($endpoint, $key, $this->http); + return new OpenAiVectorQueryService(config('services.openai'), $this->http); } throw new \Exception("No '{$service}' LLM service found"); diff --git a/app/Search/Vectors/VectorSearchRunner.php b/app/Search/Vectors/VectorSearchRunner.php index db28779e4..53b1a4bd6 100644 --- a/app/Search/Vectors/VectorSearchRunner.php +++ b/app/Search/Vectors/VectorSearchRunner.php @@ -19,6 +19,7 @@ class VectorSearchRunner $topMatches = SearchVector::query()->select('text', 'entity_type', 'entity_id') ->selectRaw('VEC_DISTANCE_COSINE(VEC_FROMTEXT("[' . implode(',', $queryVector) . ']"), embedding) as distance') ->orderBy('distance', 'asc') + ->having('distance', '<', 0.6) ->limit(10) ->get(); diff --git a/database/migrations/2025_03_24_155748_create_search_vectors_table.php b/database/migrations/2025_03_24_155748_create_search_vectors_table.php index 1b552b22c..0ae67c225 100644 --- a/database/migrations/2025_03_24_155748_create_search_vectors_table.php +++ b/database/migrations/2025_03_24_155748_create_search_vectors_table.php @@ -21,6 +21,8 @@ return new class extends Migration }); $table = DB::getTablePrefix() . 'search_vectors'; + + // TODO - Vector size might need to be dynamic DB::statement("ALTER TABLE {$table} ADD COLUMN (embedding VECTOR(1536) NOT NULL)"); DB::statement("ALTER TABLE {$table} ADD VECTOR INDEX (embedding) DISTANCE=cosine"); }