Skip to content

Commit 0ffcb3d

Browse files
committed
Vectors: Got basic LLM querying working using vector search context
1 parent 8452099 commit 0ffcb3d

8 files changed

+114
-2
lines changed

app/Search/SearchController.php

+16
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use BookStack\Entities\Queries\QueryPopular;
77
use BookStack\Entities\Tools\SiblingFetcher;
88
use BookStack\Http\Controller;
9+
use BookStack\Search\Vectors\VectorSearchRunner;
910
use Illuminate\Http\Request;
1011

1112
class SearchController extends Controller
@@ -139,4 +140,19 @@ public function searchSiblings(Request $request, SiblingFetcher $siblingFetcher)
139140

140141
return view('entities.list-basic', ['entities' => $entities, 'style' => 'compact']);
141142
}
143+
144+
public function searchQuery(Request $request, VectorSearchRunner $runner)
145+
{
146+
$query = $request->get('query', '');
147+
148+
if ($query) {
149+
$results = $runner->run($query);
150+
} else {
151+
$results = null;
152+
}
153+
154+
return view('search.query', [
155+
'results' => $results,
156+
]);
157+
}
142158
}

app/Search/Vectors/EntityVectorGenerator.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ protected function storeEmbeddings(array $embeddings, array $textChunks, Entity
4242
$toInsert[] = [
4343
'entity_id' => $entity->id,
4444
'entity_type' => $entity->getMorphClass(),
45-
'embedding' => DB::raw('STRING_TO_VECTOR("[' . implode(',', $embedding) . ']")'),
45+
'embedding' => DB::raw('VEC_FROMTEXT("[' . implode(',', $embedding) . ']")'),
4646
'text' => $text,
4747
];
4848
}

app/Search/Vectors/Services/OpenAiVectorQueryService.php

+21
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,25 @@ public function generateEmbeddings(string $text): array
3333

3434
return $response['data'][0]['embedding'];
3535
}
36+
37+
public function query(string $input, array $context): string
38+
{
39+
$formattedContext = implode("\n", $context);
40+
41+
$response = $this->jsonRequest('POST', 'v1/chat/completions', [
42+
'model' => 'gpt-4o',
43+
'messages' => [
44+
[
45+
'role' => 'developer',
46+
'content' => 'You are a helpful assistant providing search query responses. Be specific, factual and to-the-point in response.'
47+
],
48+
[
49+
'role' => 'user',
50+
'content' => "Provide a response to the below given QUERY using the below given CONTEXT\nQUERY: {$input}\n\nCONTEXT: {$formattedContext}",
51+
]
52+
],
53+
]);
54+
55+
return $response['choices'][0]['message']['content'] ?? '';
56+
}
3657
}

app/Search/Vectors/Services/VectorQueryService.php

+9
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,13 @@ interface VectorQueryService
99
* @return float[]
1010
*/
1111
public function generateEmbeddings(string $text): array;
12+
13+
/**
14+
* Query the LLM service using the given user input, and
15+
* relevant context text retrieved locally via a vector search.
16+
* Returns the response output text from the LLM.
17+
*
18+
* @param string[] $context
19+
*/
20+
public function query(string $input, array $context): string;
1221
}
+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace BookStack\Search\Vectors;
4+
5+
class VectorSearchRunner
6+
{
7+
public function __construct(
8+
protected VectorQueryServiceProvider $vectorQueryServiceProvider
9+
) {
10+
}
11+
12+
public function run(string $query): array
13+
{
14+
$queryService = $this->vectorQueryServiceProvider->get();
15+
$queryVector = $queryService->generateEmbeddings($query);
16+
17+
// TODO - Apply permissions
18+
// TODO - Join models
19+
$topMatches = SearchVector::query()->select('text', 'entity_type', 'entity_id')
20+
->selectRaw('VEC_DISTANCE_COSINE(VEC_FROMTEXT("[' . implode(',', $queryVector) . ']"), embedding) as distance')
21+
->orderBy('distance', 'asc')
22+
->limit(10)
23+
->get();
24+
25+
$matchesText = array_values(array_map(fn (SearchVector $match) => $match->text, $topMatches->all()));
26+
$llmResult = $queryService->query($query, $matchesText);
27+
28+
return [
29+
'llm_result' => $llmResult,
30+
'entity_matches' => $topMatches->toArray()
31+
];
32+
}
33+
}

database/migrations/2025_03_24_155748_create_search_vectors_table.php

+4-1
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@ public function up(): void
1616
$table->string('entity_type', 100);
1717
$table->integer('entity_id');
1818
$table->text('text');
19-
$table->vector('embedding');
2019

2120
$table->index(['entity_type', 'entity_id']);
2221
});
22+
23+
$table = DB::getTablePrefix() . 'search_vectors';
24+
DB::statement("ALTER TABLE {$table} ADD COLUMN (embedding VECTOR(1536) NOT NULL)");
25+
DB::statement("ALTER TABLE {$table} ADD VECTOR INDEX (embedding) DISTANCE=cosine");
2326
}
2427

2528
/**
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
@extends('layouts.simple')
2+
3+
@section('body')
4+
<div class="container mt-xl" id="search-system">
5+
6+
<form action="{{ url('/search/query') }}" method="get">
7+
<input name="query" type="text">
8+
<button class="button">Query</button>
9+
</form>
10+
11+
@if($results)
12+
<h2>Results</h2>
13+
14+
<h3>LLM Output</h3>
15+
<p>{{ $results['llm_result'] }}</p>
16+
17+
<h3>Entity Matches</h3>
18+
@foreach($results['entity_matches'] as $match)
19+
<div>
20+
<div><strong>{{ $match['entity_type'] }}:{{ $match['entity_id'] }}; Distance: {{ $match['distance'] }}</strong></div>
21+
<details>
22+
<summary>match text</summary>
23+
<div>{{ $match['text'] }}</div>
24+
</details>
25+
</div>
26+
@endforeach
27+
@endif
28+
</div>
29+
@stop

routes/web.php

+1
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@
187187

188188
// Search
189189
Route::get('/search', [SearchController::class, 'search']);
190+
Route::get('/search/query', [SearchController::class, 'searchQuery']);
190191
Route::get('/search/book/{bookId}', [SearchController::class, 'searchBook']);
191192
Route::get('/search/chapter/{bookId}', [SearchController::class, 'searchChapter']);
192193
Route::get('/search/entity/siblings', [SearchController::class, 'searchSiblings']);

0 commit comments

Comments
 (0)