PHPackages                             vortos/vortos-search - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. vortos/vortos-search

ActiveLibrary

vortos/vortos-search
====================

Vortos Search — enterprise-grade, event-fed unified global search. One tenant-scoped, RLS-isolated search\_document projection fed from domain events via a discoverable SearchableProjection contract (add a type = add one class). Database-agnostic driver abstraction (portable LIKE default, opt-in Postgres full-text + trigram fuzzy), permission- and owner-aware ranked query, Redis-cached, with backfill/rebuild. The app owns what is searchable and where results deep-link; the framework owns the engine.

v1.0.0-alpha-348(1mo ago)0771↓43.1%MITPHPPHP &gt;=8.2

Since Jul 18Pushed 1w agoCompare

[ Source](https://github.com/Vortos/vortos-search)[ Packagist](https://packagist.org/packages/vortos/vortos-search)[ RSS](/packages/vortos-vortos-search/feed)WikiDiscussions main Synced 2w ago

READMEChangelogDependencies (10)Versions (100)Used By (0)

vortos-search
=============

[](#vortos-search)

Enterprise-grade, event-fed **global search** for Vortos apps. One tenant-scoped, RLS-isolated `search_documents` projection, fed from your domain events through a single discoverable contract. Adding a searchable **type** is one class; new **instances** are indexed automatically because they arrive on the events you already emit.

The framework owns the engine (matching, ranking, scoping, indexing, backfill). **The app owns what is searchable and where each result deep-links** — the framework never learns what an "application" is.

Why it exists
-------------

[](#why-it-exists)

A hand-maintained search list rots the moment someone adds a feature. This package makes search a **read-model** off the event bus — the same pattern as the audit spine — so it stays correct by construction, rebuilds from scratch on demand, and scales independently of the write path.

Design at a glance
------------------

[](#design-at-a-glance)

```
domain events ──▶ SearchableProjection (app) ──▶ SearchProjectionApplier ──▶ SearchIndexWriter
                                                                                   │
                                                                          vortos_search_documents
                                                                                   │
   GET /api/search (app) ──▶ SearchQueryService ──▶ SearchReader ◀── SearchIndexDriver
                                   │                    │
                                 cache                scope (tenant + permission + owner)

```

### Scoping — two independent axes

[](#scoping--two-independent-axes)

AxisColumnEnforcement**Org**`tenant_id``WHERE tenant_id = …` **and** Postgres row-level security (`search:pg:install --rls`). An org physically cannot read another org's rows.**Member**`permission` + `owner_member_id`Org-shared rows need the caller's `permission` (or none); personal rows (`owner_member_id` set) are visible only to that member.A `SearchScope` (tenant + memberId + permissions, or `superuser`) is mandatory to read — there is no unscoped query, and the cache key folds the scope in so it can never become a bypass.

### Database-agnostic driver

[](#database-agnostic-driver)

- **`PortableLikeSearchDriver`** (default) — case-insensitive `LIKE`, any SQL engine, no special index.
- **`PostgresFtsSearchDriver`** — weighted `tsvector` (`title` A, `subtitle`/`keywords` B, `body` C)
    - `word_similarity` trigram fuzzy fallback, ranked by `ts_rank_cd`. Enable with `->driver(SearchDriver::PostgresFts)` and run `vortos:search:pg:install`.

Scoping, pagination and deep-links are identical across drivers — switching a driver changes relevance only, never who can see what. Implement `SearchIndexDriver` (+ writer/reader) to plug an external engine like OpenSearch/Meilisearch.

App integration (4 steps)
-------------------------

[](#app-integration-4-steps)

1. **Make a type searchable** — implement `SearchableProjection`; it's auto-discovered: ```
    final class ApplicationSearchProjector implements SearchableProjection
    {
        public function subscribesTo(): array { return [ApplicationSubmitted::class, ApplicationDeleted::class]; }

        public function project(object $event): SearchUpsert|SearchDelete|null
        {
            if ($event instanceof ApplicationDeleted) {
                return new SearchDelete('application', $event->id, $event->tenantId);
            }
            return new SearchUpsert(new SearchDocument(
                type: 'application', entityId: $event->id, tenantId: $event->tenantId,
                title: $event->applicantName, subtitle: $event->status,
                deeplink: "/applications/{$event->leadEntryId}",
                permission: 'entries.view.any',
                keywords: [$event->email],
            ));
        }
    }
    ```
2. **Feed the bus** — one thin Kafka handler calls `$applier->apply($event)` on your indexing consumer group (owned by the app, so consumer/topic naming stays in app config).
3. **Serve queries** — a controller builds a `SearchScope` from the authenticated principal and calls `SearchQueryService::search()`.
4. **Backfill** — implement `SearchBackfillSourceInterface` per type; run `vortos:search:rebuild`.

Configuration — `config/search.php` (all optional)
--------------------------------------------------

[](#configuration--configsearchphp-all-optional)

```
use Vortos\Search\DependencyInjection\VortosSearchConfig;
use Vortos\Search\Enum\SearchDriver;

return static function (VortosSearchConfig $config): void {
    $config
        ->driver(SearchDriver::PostgresFts)   // default: Portable
        ->rowLevelSecurity(true)              // DB-enforced org isolation
        ->cacheTtl('15 seconds')              // hot-query cache (needs a Redis SearchCacheInterface)
        ->consumer('vortos.search');          // logical consumer the app's messaging config uses
};
```

Commands
--------

[](#commands)

- `vortos:search:pg:install [--rls|--no-rls]` — Postgres extras (tsvector column + GIN + trigram + optional RLS). Idempotent.
- `vortos:search:rebuild [--type=…] [--tenant=…] [--fresh]` — (re)build the index from backfill sources.

Metrics
-------

[](#metrics)

Spread `SearchMetricDefinitions::all()` into your metrics registry (e.g. `config/metrics.php`): `search_index_upsert_total`, `search_index_delete_total`, `search_query_total`, `search_query_latency_seconds`.

###  Health Score

45

—

FairBetter than 91% of packages

Maintenance94

Actively maintained with recent releases

Popularity20

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity48

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

99

Last Release

46d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/d4a94fd7127f8bdb58b2bb2249a140569a584b67b2df184f1e8594b497d397e2?d=identicon)[Sachintha-De-Silva](/maintainers/Sachintha-De-Silva)

---

Top Contributors

[![ignis-celestis](https://avatars.githubusercontent.com/u/155328409?v=4)](https://github.com/ignis-celestis "ignis-celestis (4 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/vortos-vortos-search/health.svg)

```
[![Health](https://phpackages.com/badges/vortos-vortos-search/health.svg)](https://phpackages.com/packages/vortos-vortos-search)
```

###  Alternatives

[flow-php/flow

PHP ETL - Extract Transform Load - Data processing framework

86538.6k](/packages/flow-php-flow)[symfony/symfony

The Symfony PHP framework

31.4k87.7M2.3k](/packages/symfony-symfony)[tempest/framework

The PHP framework that gets out of your way.

2.3k42.4k21](/packages/tempest-framework)[ecotone/ecotone

Enterprise architecture layer for Laravel and Symfony — CQRS, Event Sourcing, Durable Workflows (Sagas, Orchestrators), Projections, and Outbox messaging via PHP attributes.

571604.7k69](/packages/ecotone-ecotone)[web-auth/webauthn-framework

FIDO2/Webauthn library for PHP and Symfony Bundle.

516124.5k3](/packages/web-auth-webauthn-framework)[drupal/core

Drupal is an open source content management platform powering millions of websites and applications.

19468.5M2.0k](/packages/drupal-core)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
