mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-04-19 18:22:16 +03:00
Vectors: Got basic LLM querying working using vector search context
This commit is contained in:
parent
8452099a5b
commit
0ffcb3d4aa
@ -6,6 +6,7 @@ use BookStack\Entities\Queries\PageQueries;
|
|||||||
use BookStack\Entities\Queries\QueryPopular;
|
use BookStack\Entities\Queries\QueryPopular;
|
||||||
use BookStack\Entities\Tools\SiblingFetcher;
|
use BookStack\Entities\Tools\SiblingFetcher;
|
||||||
use BookStack\Http\Controller;
|
use BookStack\Http\Controller;
|
||||||
|
use BookStack\Search\Vectors\VectorSearchRunner;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class SearchController extends Controller
|
class SearchController extends Controller
|
||||||
@ -139,4 +140,19 @@ class SearchController extends Controller
|
|||||||
|
|
||||||
return view('entities.list-basic', ['entities' => $entities, 'style' => 'compact']);
|
return view('entities.list-basic', ['entities' => $entities, 'style' => 'compact']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function searchQuery(Request $request, VectorSearchRunner $runner)
|
||||||
|
{
|
||||||
|
$query = $request->get('query', '');
|
||||||
|
|
||||||
|
if ($query) {
|
||||||
|
$results = $runner->run($query);
|
||||||
|
} else {
|
||||||
|
$results = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('search.query', [
|
||||||
|
'results' => $results,
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ class EntityVectorGenerator
|
|||||||
$toInsert[] = [
|
$toInsert[] = [
|
||||||
'entity_id' => $entity->id,
|
'entity_id' => $entity->id,
|
||||||
'entity_type' => $entity->getMorphClass(),
|
'entity_type' => $entity->getMorphClass(),
|
||||||
'embedding' => DB::raw('STRING_TO_VECTOR("[' . implode(',', $embedding) . ']")'),
|
'embedding' => DB::raw('VEC_FROMTEXT("[' . implode(',', $embedding) . ']")'),
|
||||||
'text' => $text,
|
'text' => $text,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -33,4 +33,25 @@ class OpenAiVectorQueryService implements VectorQueryService
|
|||||||
|
|
||||||
return $response['data'][0]['embedding'];
|
return $response['data'][0]['embedding'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function query(string $input, array $context): string
|
||||||
|
{
|
||||||
|
$formattedContext = implode("\n", $context);
|
||||||
|
|
||||||
|
$response = $this->jsonRequest('POST', 'v1/chat/completions', [
|
||||||
|
'model' => 'gpt-4o',
|
||||||
|
'messages' => [
|
||||||
|
[
|
||||||
|
'role' => 'developer',
|
||||||
|
'content' => 'You are a helpful assistant providing search query responses. Be specific, factual and to-the-point in response.'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'role' => 'user',
|
||||||
|
'content' => "Provide a response to the below given QUERY using the below given CONTEXT\nQUERY: {$input}\n\nCONTEXT: {$formattedContext}",
|
||||||
|
]
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $response['choices'][0]['message']['content'] ?? '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,4 +9,13 @@ interface VectorQueryService
|
|||||||
* @return float[]
|
* @return float[]
|
||||||
*/
|
*/
|
||||||
public function generateEmbeddings(string $text): array;
|
public function generateEmbeddings(string $text): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query the LLM service using the given user input, and
|
||||||
|
* relevant context text retrieved locally via a vector search.
|
||||||
|
* Returns the response output text from the LLM.
|
||||||
|
*
|
||||||
|
* @param string[] $context
|
||||||
|
*/
|
||||||
|
public function query(string $input, array $context): string;
|
||||||
}
|
}
|
||||||
|
33
app/Search/Vectors/VectorSearchRunner.php
Normal file
33
app/Search/Vectors/VectorSearchRunner.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Search\Vectors;
|
||||||
|
|
||||||
|
class VectorSearchRunner
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected VectorQueryServiceProvider $vectorQueryServiceProvider
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function run(string $query): array
|
||||||
|
{
|
||||||
|
$queryService = $this->vectorQueryServiceProvider->get();
|
||||||
|
$queryVector = $queryService->generateEmbeddings($query);
|
||||||
|
|
||||||
|
// TODO - Apply permissions
|
||||||
|
// TODO - Join models
|
||||||
|
$topMatches = SearchVector::query()->select('text', 'entity_type', 'entity_id')
|
||||||
|
->selectRaw('VEC_DISTANCE_COSINE(VEC_FROMTEXT("[' . implode(',', $queryVector) . ']"), embedding) as distance')
|
||||||
|
->orderBy('distance', 'asc')
|
||||||
|
->limit(10)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$matchesText = array_values(array_map(fn (SearchVector $match) => $match->text, $topMatches->all()));
|
||||||
|
$llmResult = $queryService->query($query, $matchesText);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'llm_result' => $llmResult,
|
||||||
|
'entity_matches' => $topMatches->toArray()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -16,10 +16,13 @@ return new class extends Migration
|
|||||||
$table->string('entity_type', 100);
|
$table->string('entity_type', 100);
|
||||||
$table->integer('entity_id');
|
$table->integer('entity_id');
|
||||||
$table->text('text');
|
$table->text('text');
|
||||||
$table->vector('embedding');
|
|
||||||
|
|
||||||
$table->index(['entity_type', 'entity_id']);
|
$table->index(['entity_type', 'entity_id']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$table = DB::getTablePrefix() . 'search_vectors';
|
||||||
|
DB::statement("ALTER TABLE {$table} ADD COLUMN (embedding VECTOR(1536) NOT NULL)");
|
||||||
|
DB::statement("ALTER TABLE {$table} ADD VECTOR INDEX (embedding) DISTANCE=cosine");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
29
resources/views/search/query.blade.php
Normal file
29
resources/views/search/query.blade.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
@extends('layouts.simple')
|
||||||
|
|
||||||
|
@section('body')
|
||||||
|
<div class="container mt-xl" id="search-system">
|
||||||
|
|
||||||
|
<form action="{{ url('/search/query') }}" method="get">
|
||||||
|
<input name="query" type="text">
|
||||||
|
<button class="button">Query</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
@if($results)
|
||||||
|
<h2>Results</h2>
|
||||||
|
|
||||||
|
<h3>LLM Output</h3>
|
||||||
|
<p>{{ $results['llm_result'] }}</p>
|
||||||
|
|
||||||
|
<h3>Entity Matches</h3>
|
||||||
|
@foreach($results['entity_matches'] as $match)
|
||||||
|
<div>
|
||||||
|
<div><strong>{{ $match['entity_type'] }}:{{ $match['entity_id'] }}; Distance: {{ $match['distance'] }}</strong></div>
|
||||||
|
<details>
|
||||||
|
<summary>match text</summary>
|
||||||
|
<div>{{ $match['text'] }}</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@stop
|
@ -187,6 +187,7 @@ Route::middleware('auth')->group(function () {
|
|||||||
|
|
||||||
// Search
|
// Search
|
||||||
Route::get('/search', [SearchController::class, 'search']);
|
Route::get('/search', [SearchController::class, 'search']);
|
||||||
|
Route::get('/search/query', [SearchController::class, 'searchQuery']);
|
||||||
Route::get('/search/book/{bookId}', [SearchController::class, 'searchBook']);
|
Route::get('/search/book/{bookId}', [SearchController::class, 'searchBook']);
|
||||||
Route::get('/search/chapter/{bookId}', [SearchController::class, 'searchChapter']);
|
Route::get('/search/chapter/{bookId}', [SearchController::class, 'searchChapter']);
|
||||||
Route::get('/search/entity/siblings', [SearchController::class, 'searchSiblings']);
|
Route::get('/search/entity/siblings', [SearchController::class, 'searchSiblings']);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user