Build related content links using vector embeddings and pgvector for Laravel.
- π Pre-computed Related Links - Related content is calculated on save, not on every page load
- π Fast Lookups - O(1) relationship queries instead of real-time similarity search
- π Cross-Model Relationships - Find related content across different model types (Blog β Events β Questions)
- π§ Multiple Embedding Providers - Support for OpenAI and Ollama
- π¦ Queue Support - Process embeddings in the background
- π Semantic Search - Search content by meaning, not just keywords
- PHP 8.3+
- Laravel 11 or 12
- PostgreSQL with pgvector extension
CREATE EXTENSION IF NOT EXISTS vector;composer require vlados/laravel-related-contentphp artisan vendor:publish --tag="related-content-config"
php artisan vendor:publish --tag="related-content-migrations"
php artisan migrate# Embedding provider (openai or ollama)
RELATED_CONTENT_PROVIDER=openai
# OpenAI settings
OPENAI_API_KEY=your-api-key
OPENAI_EMBEDDING_MODEL=text-embedding-3-small
OPENAI_EMBEDDING_DIMENSIONS=1536
# Or Ollama settings
OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_EMBEDDING_MODEL=nomic-embed-textuse Vlados\LaravelRelatedContent\Concerns\HasRelatedContent;
class BlogPost extends Model
{
use HasRelatedContent;
/**
* Define which fields should be embedded.
*/
public function embeddableFields(): array
{
return ['title', 'excerpt', 'content'];
}
}In config/related-content.php:
'models' => [
\App\Models\BlogPost::class,
\App\Models\Event::class,
\App\Models\Question::class,
],$post = BlogPost::create([
'title' => 'Electric Vehicle Charging Guide',
'content' => '...',
]);
// Embedding is generated and related content is found automatically// Get all related content
$related = $post->getRelatedModels();
// Get related content of a specific type
$relatedEvents = $post->getRelatedOfType(Event::class);
// Get the raw relationship with similarity scores
$post->relatedContent()->with('related')->get();@if($post->relatedContent->isNotEmpty())
<div class="related-content">
<h3>Related Content</h3>
@foreach($post->getRelatedModels(5) as $item)
<a href="{{ $item->url }}">{{ $item->title }}</a>
@endforeach
</div>
@endif# Process models missing embeddings (default behavior)
php artisan related-content:rebuild
# Process a specific model (missing only)
php artisan related-content:rebuild "App\Models\BlogPost"
# Force regenerate all embeddings
php artisan related-content:rebuild --force
# Process synchronously (instead of queuing)
php artisan related-content:rebuild --sync
# With custom chunk size
php artisan related-content:rebuild --chunk=50You can also use the package for semantic search:
use Vlados\LaravelRelatedContent\Services\RelatedContentService;
$service = app(RelatedContentService::class);
// Search across all embeddable models
$results = $service->search('electric vehicle charging');
// Search specific model types
$results = $service->search('charging stations', [
\App\Models\Event::class,
\App\Models\BlogPost::class,
]);return [
// Embedding provider: 'openai' or 'ollama'
'provider' => env('RELATED_CONTENT_PROVIDER', 'openai'),
// Provider-specific settings
'providers' => [
'openai' => [
'api_key' => env('OPENAI_API_KEY'),
'base_url' => env('OPENAI_BASE_URL', 'https://api.openai.com/v1'),
'model' => env('OPENAI_EMBEDDING_MODEL', 'text-embedding-3-small'),
'dimensions' => env('OPENAI_EMBEDDING_DIMENSIONS', 1536),
],
'ollama' => [
'base_url' => env('OLLAMA_BASE_URL', 'http://localhost:11434'),
'model' => env('OLLAMA_EMBEDDING_MODEL', 'nomic-embed-text'),
'dimensions' => env('OLLAMA_EMBEDDING_DIMENSIONS', 768),
],
],
// Maximum related items per model
'max_related_items' => 10,
// Minimum similarity threshold (0-1)
'similarity_threshold' => 0.5,
// Queue settings
'queue' => [
'connection' => 'default',
'name' => 'default',
],
// Models to include in cross-model relationships
'models' => [],
// Database table names
'tables' => [
'embeddings' => 'embeddings',
'related_content' => 'related_content',
],
];The package dispatches events you can listen to:
use Vlados\LaravelRelatedContent\Events\RelatedContentSynced;
class HandleRelatedContentSynced
{
public function handle(RelatedContentSynced $event): void
{
// $event->model - The model that was synced
}
}- On Model Save: When a model with
HasRelatedContentis saved, a job is dispatched - Generate Embedding: The job generates a vector embedding from the model's embeddable fields
- Find Similar: Uses pgvector to find similar content across all configured models
- Store Links: Stores the related content relationships in the
related_contenttable - Fast Retrieval: When displaying related content, it's a simple database lookup (no API calls)
Related content works in both directions automatically. When a new BlogPost is saved and finds an Event as related, the Event will also show the BlogPost in its related content - without needing to re-sync the Event.
This is achieved by querying both directions:
- Forward: where this model is the source
- Reverse: where this model is the related target
Results are deduplicated and sorted by similarity score.
- Embedding Generation: ~200-500ms per model (depends on text length and provider)
- Related Content Lookup: ~5ms (simple database query)
- Storage: ~6KB per embedding (1536 dimensions x 4 bytes)
MIT License. See LICENSE for more information.