PHPackages                             ginkelsoft/laravel-encrypted-search-index - 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. [Database &amp; ORM](/categories/database)
4. /
5. ginkelsoft/laravel-encrypted-search-index

ActiveLibrary[Database &amp; ORM](/categories/database)

ginkelsoft/laravel-encrypted-search-index
=========================================

A lightweight Laravel package that provides privacy-preserving, encrypted fulltext and prefix search support for Eloquent models.

v1.0.11(7mo ago)258↓50%[1 issues](https://github.com/ginkelsoft-development/laravel-encrypted-search-index/issues)MITPHPPHP ^8.1 || ^8.2 || ^8.3 || ^8.4CI passing

Since Oct 9Pushed 7mo agoCompare

[ Source](https://github.com/ginkelsoft-development/laravel-encrypted-search-index)[ Packagist](https://packagist.org/packages/ginkelsoft/laravel-encrypted-search-index)[ RSS](/packages/ginkelsoft-laravel-encrypted-search-index/feed)WikiDiscussions develop Synced 1mo ago

READMEChangelog (10)Dependencies (5)Versions (33)Used By (0)

Ginkelsoft Laravel Encrypted Search Index
=========================================

[](#ginkelsoft-laravel-encrypted-search-index)

[![Tests](https://github.com/ginkelsoft-development/laravel-encrypted-search-index/actions/workflows/tests.yml/badge.svg)](https://github.com/ginkelsoft-development/laravel-encrypted-search-index/actions/workflows/tests.yml)[![Latest Version on Packagist](https://camo.githubusercontent.com/6dbec500e176a6c3ce0421029bb7ee56297024a7ca50335aa1abfe7dac7ca2af/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f67696e6b656c736f66742f6c61726176656c2d656e637279707465642d7365617263682d696e6465782e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/ginkelsoft/laravel-encrypted-search-index)[![Total Downloads](https://camo.githubusercontent.com/d74868764435cbab06f47268544a61e1a5b923ee00e1fe7926a4cbb94b890a57/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f67696e6b656c736f66742f6c61726176656c2d656e637279707465642d7365617263682d696e6465782e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/ginkelsoft/laravel-encrypted-search-index)[![License](https://camo.githubusercontent.com/2b402f4e79632cf70e32e81507982798e8cf0477b4a5c1774731ba7f919b655e/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f67696e6b656c736f66742d646576656c6f706d656e742f6c61726176656c2d656e637279707465642d7365617263682d696e6465782e7376673f7374796c653d666c61742d737175617265)](LICENSE.md)[![Laravel](https://camo.githubusercontent.com/81ed56d97cf81b511749101fbfe1fe990cafa9dfade49b7172818263cf215f38/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c61726176656c2d382d2d31322d627269676874677265656e3f7374796c653d666c61742d737175617265266c6f676f3d6c61726176656c)](https://laravel.com)[![PHP](https://camo.githubusercontent.com/063ff3575c294d8097afe0e6deb9bed4bc8afc58ae492aef3c7dce8f66cba02c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e312532302d2d253230382e342d626c75653f7374796c653d666c61742d737175617265266c6f676f3d706870)](https://php.net)[![Elasticsearch](https://camo.githubusercontent.com/3c7bdf1932218ea39ae12a524bc3eae9f10c71bd0160c71c65260bb3a190d128/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5365617263682d44422532306f72253230456c61737469637365617263682d6666393930303f7374796c653d666c61742d737175617265266c6f676f3d656c6173746963736561726368)](#elasticsearch-integration)

Overview
--------

[](#overview)

Modern applications that handle sensitive user data—such as healthcare, financial, or membership systems—must ensure that all personally identifiable information (PII) is properly encrypted at rest. However, standard encryption creates a practical challenge: **once data is encrypted, it can no longer be searched efficiently.**

Laravel's built-in `Crypt` system offers strong encryption (AES-256-CBC) but provides no mechanism for searching encrypted values. Some systems attempt to address this by storing partial plaintext or using blind indexes, which can leak statistical patterns and increase the risk of correlation attacks.

The **Laravel Encrypted Search Index** package provides a clean, secure, and scalable alternative. It allows encrypted model fields to be **searched using deterministic hashed tokens**, without ever exposing plaintext data.

---

Problem Statement
-----------------

[](#problem-statement)

### The traditional trade-off

[](#the-traditional-trade-off)

When data is fully encrypted, you lose the ability to perform meaningful queries. Developers must choose between:

1. **Strong security (no search):** Encrypt every value with a random IV; searches become impossible.
2. **Weak security (searchable):** Store hashed or partially-encrypted values that can be compared, leaking patterns.

This package removes that trade-off by introducing a **detached searchable index** that maps encrypted records to deterministic tokens.

---

Key Features
------------

[](#key-features)

- **Searchable encryption** — Enables exact and prefix-based searches over encrypted data.
- **Detached search index** — Tokens are stored separately from the main data, reducing exposure risk.
- **Deterministic hashing with peppering** — Each token is derived from normalized text combined with a secret pepper.
- **No blind indexes in primary tables** — Encrypted fields remain opaque; only hashed references are stored elsewhere.
- **High scalability** — Efficient for millions of records through database indexing or Elasticsearch.
- **Elasticsearch integration** — Optionally store and query search tokens directly in an Elasticsearch index.
- **Laravel-native integration** — Works directly with Eloquent models, query scopes, and model events.
- **Automatic field detection** — Automatically indexes fields that use an encrypted cast when enabled.
- **Fine-grained configuration** — Supports attributes (`#[EncryptedSearch]`) and `$encryptedSearch` arrays for per-field behavior.

---

How It Works
------------

[](#how-it-works)

Each model can declare specific fields as searchable. When the model is saved, the system normalizes the field value, generates one or more hashed tokens, and stores them in a separate table named `encrypted_search_index` **or** in an Elasticsearch index if configured.

When you search, the package hashes your input using the same process and retrieves matching model IDs from the index.

### 1. Token Generation

[](#1-token-generation)

For each configured field:

- **Exact match token:** A SHA-256 hash of the normalized value + secret pepper.
- **Prefix tokens:** Multiple SHA-256 hashes representing progressive prefixes of the normalized text (e.g. `w`, `wi`, `wie`).

### 2. Token Storage

[](#2-token-storage)

By default, all tokens are stored in the database table `encrypted_search_index`. When Elasticsearch is enabled, they are stored in the configured Elasticsearch index instead.

Example structure:

model\_typemodel\_idfieldtypetokenApp\\Models\\Client42last\_namesexact\[hash\]App\\Models\\Client42last\_namesprefix\[hash\]### 3. Querying

[](#3-querying)

The package provides two Eloquent scopes:

```
Client::encryptedExact('last_names', 'Vermeer')->get();
Client::encryptedPrefix('first_names', 'Wie')->get();
```

These use indexed lookups (DB or Elasticsearch) and remain performant even at scale.

---

Elasticsearch Integration
-------------------------

[](#elasticsearch-integration)

### Enabling Elasticsearch

[](#enabling-elasticsearch)

To enable Elasticsearch as the storage and query backend for encrypted tokens, set the following in your `.env` file:

```
ENCRYPTED_SEARCH_DRIVER=elasticsearch
ELASTICSEARCH_HOST=http://localhost:9200
ELASTICSEARCH_INDEX=encrypted_search

```

In `config/encrypted-search.php`:

```
return [
    'search_pepper' => env('SEARCH_PEPPER', ''),
    'max_prefix_depth' => 6,

    'elasticsearch' => [
        'enabled' => env('ENCRYPTED_SEARCH_DRIVER', 'database') === 'elasticsearch',
        'host' => env('ELASTICSEARCH_HOST', 'http://localhost:9200'),
        'index' => env('ELASTICSEARCH_INDEX', 'encrypted_search'),
    ],
];
```

When enabled, the package will **skip database writes** to `encrypted_search_index` and instead sync tokens directly to Elasticsearch via the `ElasticsearchService`.

### Searching via Elasticsearch

[](#searching-via-elasticsearch)

To manually query Elasticsearch for a specific token:

```
curl -X GET "http://localhost:9200/encrypted_search/_search?pretty" \
-H 'Content-Type: application/json' \
-d '{
  "query": {
    "term": {
      "token.keyword": ""
    }
  }
}'
```

Both the database and Elasticsearch drivers use the same search scopes — your application code remains identical regardless of which backend is active.

For prefix-based queries, you can match multiple tokens:

```
curl -X GET "http://localhost:9200/encrypted_search/_search?pretty" \
-H 'Content-Type: application/json' \
-d '{
  "query": {
    "bool": {
      "should": [
        { "terms": { "token.keyword": ["token1", "token2", "token3"] } }
      ]
    }
  }
}'
```

The same token-based hashing rules apply — plaintext values must first be converted into deterministic tokens.

---

Security Model
--------------

[](#security-model)

ThreatMitigationDatabase dump or breachTokens cannot be reversed (salted + peppered SHA-256).Statistical analysisTokens are detached; frequency analysis yields no correlation.Insider accessNo sensitive data in index table; encrypted fields remain opaque.Leaked `APP_KEY`Irrelevant for tokens; pepper is stored separately in `.env`.This design follows a **defense-in-depth** model: encrypted data stays secure, while search operations remain practical.

---

Installation
------------

[](#installation)

```
composer require ginkelsoft/laravel-encrypted-search-index
php artisan vendor:publish --provider="Ginkelsoft\EncryptedSearch\EncryptedSearchServiceProvider" --tag=config
php artisan vendor:publish --provider="Ginkelsoft\EncryptedSearch\EncryptedSearchServiceProvider" --tag=migrations
php artisan migrate
```

If you plan to use the Elasticsearch integration, make sure an Elasticsearch instance (version **8.x or newer**) is running and accessible at the host defined in your `.env` file.

Then add a unique pepper to your `.env` file:

```
SEARCH_PEPPER=your-random-secret-string

```

---

Configuration
-------------

[](#configuration)

`config/encrypted-search.php`:

```
return [
    // Secret pepper for token hashing
    'search_pepper' => env('SEARCH_PEPPER', ''),

    // Maximum prefix depth for token generation
    'max_prefix_depth' => 6,

    // Minimum prefix length for search queries (default: 3)
    'min_prefix_length' => env('ENCRYPTED_SEARCH_MIN_PREFIX', 3),

    // Automatic indexing of encrypted casts
    'auto_index_encrypted_casts' => true,

    // Elasticsearch integration
    'elasticsearch' => [
        'enabled' => env('ENCRYPTED_SEARCH_ELASTIC_ENABLED', false),
        'host' => env('ELASTICSEARCH_HOST', 'http://elasticsearch:9200'),
        'index' => env('ELASTICSEARCH_INDEX', 'encrypted_search'),
    ],

    // Debug logging
    'debug' => env('ENCRYPTED_SEARCH_DEBUG', false),
];
```

### Configuration Options

[](#configuration-options)

OptionDefaultDescription`search_pepper``''`Secret pepper value for token hashing. **Required for security.**`max_prefix_depth``6`Maximum number of prefix characters to index (e.g., "wietse" → w, wi, wie, wiet, wiets, wietse)`min_prefix_length``3`Minimum search term length for prefix queries. Prevents overly broad matches from short terms like "w" or "de".`auto_index_encrypted_casts``true`Automatically index fields with `encrypted` cast types`elasticsearch.enabled``false`Use Elasticsearch instead of database for token storage`elasticsearch.host``http://elasticsearch:9200`Elasticsearch host URL`elasticsearch.index``encrypted_search`Elasticsearch index name`debug``false`Enable debug logging for index operations### Minimum Prefix Length

[](#minimum-prefix-length)

The `min_prefix_length` setting prevents performance issues and false positives from very short search terms.

**Example with `min_prefix_length = 3` (default):**

```
// ❌ Returns no results (too short)
Client::encryptedPrefix('first_names', 'Wi')->get();

// ✅ Works normally (meets minimum)
Client::encryptedPrefix('first_names', 'Wil')->get();  // Finds "Wilma"

// ✅ Exact search always works (ignores minimum)
Client::encryptedExact('first_names', 'Wi')->get();
```

**Recommended values:**

- `1`: Allow single-character searches (more flexible, more false positives)
- `2`: Require two characters (good for short names)
- `3`: Require three characters (recommended - good balance)
- `4`: Require four characters (very precise, less flexible)

To adjust this setting, add to your `.env`:

```
ENCRYPTED_SEARCH_MIN_PREFIX=3
```

---

Usage
-----

[](#usage)

### Model Setup

[](#model-setup)

If `auto_index_encrypted_casts` is enabled in the configuration (default: **true**), all model fields that use an `encrypted:` cast will be automatically indexed for exact search, even if they are not explicitly listed in `$encryptedSearch`.

You can also use PHP attributes to control search behavior per field:

```
use Ginkelsoft\EncryptedSearch\Attributes\EncryptedSearch;

class Client extends Model
{
    #[EncryptedSearch(exact: true, prefix: true)]
    public string $last_names;
}

When a record is saved, searchable tokens are automatically generated in `encrypted_search_index` or synced to Elasticsearch.

### Searching

#### Single Field Search

```php
// Exact match on a single field
$clients = Client::encryptedExact('last_names', 'Vermeer')->get();

// Prefix match on a single field
$clients = Client::encryptedPrefix('first_names', 'Wie')->get();
```

#### Multi-Field Search

[](#multi-field-search)

Search across multiple fields simultaneously using OR logic:

```
// Exact match across multiple fields
// Finds records where 'John' appears in first_names OR last_names
$clients = Client::encryptedExactMulti(['first_names', 'last_names'], 'John')->get();

// Prefix match across multiple fields
// Finds records where 'Wie' is a prefix of first_names OR last_names
$clients = Client::encryptedPrefixMulti(['first_names', 'last_names'], 'Wie')->get();
```

**Use cases for multi-field search:**

- Search for a name that could be in either first name or last name fields
- Search across multiple encrypted fields without multiple queries
- Implement autocomplete across multiple fields
- Unified search experience across related fields

**Note:** Multi-field searches automatically deduplicate results, so if a record matches in multiple fields, it will only appear once in the results.

Attributes always override global or $encryptedSearch configuration for the same field.

---

#### ✅ 3. **Configuration block (insert this before `'elasticsearch' => [...]`)**

[](#-3-configuration-block-insert-this-before-elasticsearch--)

```
'auto_index_encrypted_casts' => true,

## Rebuilding or Syncing the Search Index
This command automatically detects whether you are using the database or Elasticsearch driver,
and rebuilds the appropriate index accordingly.

Rebuild indexes via Artisan:

```bash
php artisan encryption:index-rebuild "App\\Models\\Client"
```

If Elasticsearch is enabled, this will repopulate the Elasticsearch index instead of the database.

---

Scalability and Performance
---------------------------

[](#scalability-and-performance)

- **Indexed database or Elasticsearch lookups** for efficient token search.
- **Chunked rebuilds** for large datasets (`--chunk` option).
- **Queue-compatible** for asynchronous index rebuilds.
- **Elasticsearch mode** scales horizontally for enterprise use.

The detached index structure scales linearly and supports millions of records efficiently.

---

Framework Compatibility
-----------------------

[](#framework-compatibility)

Laravel VersionSupported PHP Versions**8.x**8.0 – 8.1**9.x**8.1 – 8.2**10.x**8.1 – 8.3**11.x**8.2 – 8.3**12.x**8.3 and higherThe package is continuously tested across all supported combinations using GitHub Actions.

---

Compliance
----------

[](#compliance)

- **GDPR** — Encrypted and hashed separation ensures minimal data exposure.
- **HIPAA** — Meets encryption-at-rest requirements for ePHI.
- **ISO 27001** — Aligns with confidentiality and cryptographic control standards.

---

Troubleshooting
---------------

[](#troubleshooting)

**ConnectionException (cURL error 7)**
Ensure your Elasticsearch container or service is running and reachable at the configured `ELASTICSEARCH_HOST`.

**Missing index mappings**
If you haven’t created the Elasticsearch index yet, initialize it manually:

```
curl -X PUT http://localhost:9200/encrypted_search

## License

MIT License
(c) 2025 Ginkelsoft
```

###  Health Score

38

—

LowBetter than 85% of packages

Maintenance55

Moderate activity, may be stable

Popularity13

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity64

Established project with proven stability

 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

12

Last Release

210d ago

PHP version history (3 changes)v1.0.1PHP ^7.4 || ^8.0 || ^8.1 || ^8.2 || ^8.3

v1.0.2PHP ^8.1

v1.0.4PHP ^8.1 || ^8.2 || ^8.3 || ^8.4

### Community

Maintainers

![](https://www.gravatar.com/avatar/e1809d1eece1aa425a83ad45ac6ac32a050e4d40006abe460eb115a5e636fbfe?d=identicon)[ginkelsoft](/maintainers/ginkelsoft)

---

Top Contributors

[![ginkelsoft-development](https://avatars.githubusercontent.com/u/179240029?v=4)](https://github.com/ginkelsoft-development "ginkelsoft-development (71 commits)")

---

Tags

blind-indexdatabase-securityelasticsearcheloquentencrypted-searchencryptiongdprhashinglaravellaravel-packagelaravel-securityphpsearch-indexsearchable-encryptionlaraveleloquentindexingprivacyencrypted searchginkelsoft

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/ginkelsoft-laravel-encrypted-search-index/health.svg)

```
[![Health](https://phpackages.com/badges/ginkelsoft-laravel-encrypted-search-index/health.svg)](https://phpackages.com/packages/ginkelsoft-laravel-encrypted-search-index)
```

###  Alternatives

[watson/validating

Eloquent model validating trait.

9723.3M47](/packages/watson-validating)[plank/laravel-mediable

A package for easily uploading and attaching media files to models with Laravel

8271.5M11](/packages/plank-laravel-mediable)[cybercog/laravel-love

Make Laravel Eloquent models reactable with any type of emotions in a minutes!

1.2k302.7k1](/packages/cybercog-laravel-love)[cviebrock/eloquent-taggable

Easy ability to tag your Eloquent models in Laravel.

567694.8k3](/packages/cviebrock-eloquent-taggable)[reedware/laravel-relation-joins

Adds the ability to join on a relationship by name.

2121.2M13](/packages/reedware-laravel-relation-joins)[highsolutions/eloquent-sequence

A Laravel package for easy creation and management sequence support for Eloquent models with elastic configuration.

121130.3k](/packages/highsolutions-eloquent-sequence)

PHPackages © 2026

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