Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Postgres Search Bundle Laravel Package

gkovrey/postgres-search-bundle

gkovrey/postgres-search-bundle integrates PostgreSQL full-text search into Symfony/PHP applications, offering database-native indexing and query support for fast, relevant search results without an external search engine. Configurable, lightweight, and easy to wire into existing projects.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check first: this is a Symfony2 bundle (2016, zero stars, no dependents). It cannot be installed into a Laravel app — the bundle class extends Symfony\Component\HttpKernel\Bundle\Bundle, its service wiring lives in Resources/config/services.xml, and it assumes the Symfony DI container and DoctrineBundle. Treat the repo as reference material for the SQL, not as a dependency.

Minimal steps for a Laravel developer:

  1. Read the Doctrine DQL function definitions in the repo — that is where the actual value is. They show the canonical Postgres snippets (to_tsvector, plainto_tsquery, ts_rank, ts_headline) and how to bind them into a query builder.
  2. Decide whether you are using Eloquent or laravel-doctrine/orm. If Eloquent, skip the package entirely and copy the SQL. If Doctrine, register the functions by hand:
// config/doctrine.php  (laravel-doctrine)
'custom_string_functions' => [
    'TO_TSVECTOR' => \App\Doctrine\ToTsVector::class,
],
'custom_numeric_functions' => [
    'TS_RANK' => \App\Doctrine\TsRank::class,
],
  1. First use case: full-text search over one existing table. Add a tsvector column, backfill it, add a GIN index, and expose a single Eloquent scope. Do this before touching rank weighting or highlighting.
// migration
DB::statement("ALTER TABLE posts ADD COLUMN search_vector tsvector");
DB::statement("UPDATE posts SET search_vector =
    setweight(to_tsvector('english', coalesce(title,'')), 'A') ||
    setweight(to_tsvector('english', coalesce(body,'')), 'B')");
DB::statement("CREATE INDEX posts_search_idx ON posts USING GIN (search_vector)");

Implementation Patterns

Scope-based search. Wrap the raw SQL in a query scope so controllers never see whereRaw. This is the Laravel equivalent of the bundle's query helpers:

public function scopeSearch(Builder $q, string $term): Builder
{
    return $q->whereRaw('search_vector @@ websearch_to_tsquery(?, ?)', ['english', $term])
             ->orderByRaw('ts_rank_cd(search_vector, websearch_to_tsquery(?, ?)) DESC', ['english', $term]);
}

websearch_to_tsquery (PG 11+) accepts user-shaped input ("quoted phrases", or, -exclude) and never throws on malformed syntax — much better than feeding raw input to to_tsquery, which is what 2016-era code did.

Keep the vector current with a trigger, not model events. Eloquent saved hooks miss bulk update(), seeders, and other services writing to the table:

CREATE TRIGGER posts_search_update BEFORE INSERT OR UPDATE ON posts
FOR EACH ROW EXECUTE FUNCTION
tsvector_update_trigger(search_vector, 'pg_catalog.english', title, body);

Note tsvector_update_trigger only handles plain columns (no setweight); if you need weights A/B, write a small custom PL/pgSQL function instead.

Weighted fields + headline. Mirror the bundle's intent: title outranks body, and return a snippet. ts_headline is expensive — apply it only to the current page of results, in PHP or in a second pass, not in the main ranked query.

Short-query fallback. FTS ignores stopwords and doesn't do prefix matching well. For queries under ~3 chars or "did you mean" behavior, branch to ILIKE '%term%' or pg_trgm with a separate GIN index — the bundle has no answer for this and you will hit it in production.

Language as configuration. Never hardcode 'english' in scopes. Inject a config value so a multi-tenant app can use 'simple' (no stemming) or 'russian' where appropriate; the regconfig must be a constant in an expression index, so this decision is baked into the schema.

Gotchas and Tips

  • Framework mismatch is the headline. Nothing here is installable in Laravel. Even under laravel-doctrine, the bundle's DependencyInjection/ layer won't run; only the DQL function classes port cleanly.
  • Immutable config requirement. CREATE INDEX ... (to_tsvector(col)) fails unless the regconfig is a literal. Use to_tsvector('english'::regconfig, col) or, cleaner, a stored generated column (PG 12+): GENERATED ALWAYS AS (to_tsvector('english', title || ' ' || body)) STORED.
  • to_tsvector is not immutable with a column-based config, so generated columns are the only way to index a per-row language safely.
  • Rank is not relevance-tuned out of the box. ts_rank vs ts_rank_cd differ noticeably; ts_rank_cd handles proximity better for long documents. Benchmark on real data, don't trust the demo.
  • Empty or all-stopword queries return zero rows silently ("the", "a"). Detect this before querying and either fall back to ILIKE or return an explanatory message.
  • Debug with EXPLAIN (ANALYZE, BUFFERS). If you see Seq Scan instead of Bitmap Index Scan on the GIN index, you're either missing the index, using a mismatched regconfig, or the planner is choosing a scan because the table is tiny in dev.
  • ts_headline is the #1 performance trap — it re-parses the document per row. Never put it in a WHERE/ORDER BY path.
  • Transaction visibility in tests. RefreshDatabase + trigger-maintained vectors work, but if you backfill via DB::statement in a migration, RefreshDatabase on SQLite will fail — you need a real Postgres test database (pgsql connection, separate phpunit.xml env).
  • Extension points you'd actually want: a Searchable trait that declares searchableColumns() and renders the trigger SQL, plus a SearchResult DTO carrying rank and headline. Neither exists here, so budget an afternoon to write them.
  • Maintenance risk: with zero dependents and no commits since 2016, assume no fixes for PG 11+ (websearch_to_tsquery), PG 12+ (generated columns), or PG 15+ behavior changes. Pin your own SQL and own it.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core