use BookStack\Entities\Queries\QueryPopular;
use BookStack\Entities\Tools\SiblingFetcher;
use BookStack\Http\Controller;
+use BookStack\Search\Vectors\VectorSearchRunner;
use Illuminate\Http\Request;
class SearchController extends Controller
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,
+ ]);
+ }
}
$toInsert[] = [
'entity_id' => $entity->id,
'entity_type' => $entity->getMorphClass(),
- 'embedding' => DB::raw('STRING_TO_VECTOR("[' . implode(',', $embedding) . ']")'),
+ 'embedding' => DB::raw('VEC_FROMTEXT("[' . implode(',', $embedding) . ']")'),
'text' => $text,
];
}
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'] ?? '';
+ }
}
* @return float[]
*/
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;
}
--- /dev/null
+<?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()
+ ];
+ }
+}
$table->string('entity_type', 100);
$table->integer('entity_id');
$table->text('text');
- $table->vector('embedding');
$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");
}
/**
--- /dev/null
+@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
// 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/chapter/{bookId}', [SearchController::class, 'searchChapter']);
Route::get('/search/entity/siblings', [SearchController::class, 'searchSiblings']);