databases storage and caching

Search Orientation

Search lets users find records by words, filters, categories, dates, and relevance instead of exact IDs. In PHP applications, search often starts with the database and later moves to a dedicated search engine when the product needs better ranking, typo tolerance, filtering, analytics, or scale.

The main skill is choosing the right level of search for the problem. A junior developer should understand simple database search, full-text search, external indexes, eventual consistency, and the fact that the database usually remains the source of truth.

Search Is Not Only Matching Text

Search usually combines several behaviours:

  • text matching, such as searching product names for lamp;
  • filters, such as category, status, price range, or owner;
  • sorting, such as newest first or most relevant first;
  • pagination, so one request does not return every result;
  • access control, so users only see records they are allowed to see.

This means search code is not just "find rows with this word". It is a user-facing query boundary.

PHP example
<?php

declare(strict_types=1);

function normaliseSearchTerm(string $term): string
{
    return trim(preg_replace('/\s+/', ' ', $term) ?? '');
}

echo normaliseSearchTerm('  desk    lamp  ') . PHP_EOL;

// Prints:
// desk lamp

Normalising the input gives the rest of the search code a cleaner value to work with. It does not replace prepared statements, authorisation, or proper query design.

Database search is often the right starting point. It keeps the system simple because the application already reads from the database.

For small tables, exact filters and simple LIKE searches may be enough. For larger text search, MySQL, MariaDB, and PostgreSQL also provide full-text features. The exact SQL differs by database, but the idea is the same: ask the database to find rows that match text.

PHP example
<?php

declare(strict_types=1);

function databaseSearchPlan(string $term, string $status): array
{
    return [
        'strategy' => 'database',
        'filters' => [
            'term' => normaliseSearchTerm($term),
            'status' => $status,
        ],
        'sort' => 'newest first',
        'limit' => 25,
    ];
}

print_r(databaseSearchPlan(' desk lamp ', 'published'));

// Prints:
// [strategy] => database
// [term] => desk lamp
// [status] => published
// [limit] => 25

Database search is a good fit when the dataset is modest, the ranking rules are simple, and the product can live without advanced typo tolerance or relevance tuning.

Prepared Search Queries

Search input must be treated as untrusted input. Do not concatenate the raw search term into SQL. Use prepared statements and bind values.

PHP example
<?php

declare(strict_types=1);

function productLikeSearchParameters(string $term): array
{
    $normalised = normaliseSearchTerm($term);

    return [
        'sql' => 'SELECT id, name FROM products WHERE status = :status AND name LIKE :term LIMIT 25',
        'params' => [
            'status' => 'published',
            'term' => '%' . $normalised . '%',
        ],
    ];
}

print_r(productLikeSearchParameters('lamp'));

// Prints:
// [status] => published
// [term] => %lamp%

This example returns a query shape instead of connecting to a database. In real code, pass the SQL and parameters to PDO or the framework query builder.

When A Search Engine Helps

Dedicated search engines such as Meilisearch, Elasticsearch, and OpenSearch are useful when search becomes a product feature in its own right.

They can help with:

  • relevance ranking;
  • typo tolerance and synonyms;
  • faceted filters such as brand, size, colour, or category;
  • fast search over large datasets;
  • highlighting matched text;
  • indexing documents from multiple tables or services;
  • analytics about what users search for.

Meilisearch is often simpler to operate and a good fit for product search, documentation, and smaller teams. Elasticsearch and OpenSearch are more complex but powerful for large search, logs, analytics, and advanced query needs.

The Search Index Is A Copy

An external search engine does not replace the database. It stores a searchable copy of selected fields. The database remains the source of truth.

PHP example
<?php

declare(strict_types=1);

function productSearchDocument(array $product): array
{
    return [
        'id' => (int) $product['id'],
        'name' => (string) $product['name'],
        'description' => (string) $product['description'],
        'category' => (string) $product['category'],
        'status' => (string) $product['status'],
        'price_cents' => (int) $product['price_cents'],
    ];
}

$document = productSearchDocument([
    'id' => 42,
    'name' => 'Desk lamp',
    'description' => 'Adjustable lamp for a home office.',
    'category' => 'lighting',
    'status' => 'published',
    'price_cents' => 3999,
]);

echo $document['name'] . ' in ' . $document['category'] . PHP_EOL;

// Prints:
// Desk lamp in lighting

The document should include fields needed for search, filtering, and displaying results. Avoid indexing private fields unless the search system is designed to protect them.

Indexing And Eventual Consistency

When a product is updated in the database, the search index must be updated too. That may happen immediately in the request, or asynchronously through a queue.

Asynchronous indexing is common because it keeps user requests fast. The tradeoff is eventual consistency: the database may change before search results catch up.

PHP example
<?php

declare(strict_types=1);

function indexingPlan(bool $needsImmediateSearchUpdate): string
{
    if ($needsImmediateSearchUpdate) {
        return 'update the database, then update the search index before confirming the change';
    }

    return 'update the database, then queue a job to refresh the search index';
}

echo indexingPlan(false) . PHP_EOL;

// Prints:
// update the database, then queue a job to refresh the search index

In many applications, a short delay is acceptable. For example, a product edit appearing in search a few seconds later is usually fine. A permission change may need stricter handling.

Search Results Still Need Authorisation

Search engines are good at finding matching documents. They are not a replacement for application authorisation.

If private records are indexed, the search query must include the user's allowed scope, or the application must filter results before showing them. The best design is usually to avoid indexing data into a place where the wrong user could query it.

PHP example
<?php

declare(strict_types=1);

function searchFiltersForUser(int $userId, bool $isAdmin): array
{
    if ($isAdmin) {
        return ['status' => 'published'];
    }

    return [
        'status' => 'published',
        'visible_to_user_id' => $userId,
    ];
}

print_r(searchFiltersForUser(42, false));

// Prints:
// [status] => published
// [visible_to_user_id] => 42

Search bugs can become data leaks. Treat filters and permissions as part of the search design, not as a later UI concern.

Pagination And Result Hydration

Search engines often return IDs plus ranking information. The application may then load full records from the database using those IDs. This is called hydration.

Hydration keeps search documents smaller and ensures the final display uses current database data. It also means the code must preserve result order and handle IDs that no longer exist.

PHP example
<?php

declare(strict_types=1);

function preserveSearchOrder(array $searchIds, array $databaseRowsById): array
{
    $ordered = [];

    foreach ($searchIds as $id) {
        if (isset($databaseRowsById[$id])) {
            $ordered[] = $databaseRowsById[$id];
        }
    }

    return $ordered;
}

$rows = preserveSearchOrder(
    [9, 4, 7],
    [
        4 => ['id' => 4, 'name' => 'Chair'],
        7 => ['id' => 7, 'name' => 'Table'],
        9 => ['id' => 9, 'name' => 'Lamp'],
    ]
);

echo implode(', ', array_column($rows, 'name')) . PHP_EOL;

// Prints:
// Lamp, Chair, Table

This is a common real-world detail: the database query may not naturally return rows in the same order as the search engine.

What To Check

Before moving on, make sure you can:

  • explain when simple database search is enough;
  • describe why a search engine index is a copy, not the source of truth;
  • prepare search inputs safely instead of concatenating SQL;
  • choose fields that belong in a search document;
  • account for eventual consistency when indexing through queues;
  • include authorisation and visibility in search design;
  • preserve result order when hydrating search IDs from the database.

Practice

Practice: Plan A Product Search Flow

Build a small PHP model for deciding how product search should work.

Requirements

  • Normalise a search term by trimming extra whitespace.
  • Choose database search for a small catalogue with simple ranking.
  • Choose a search engine when typo tolerance, facets, or advanced ranking are required.
  • Build a search document from a product array.
  • Include visibility or status information so private records are not accidentally exposed.
  • Show how search result IDs can be hydrated in the original search order.

Do not connect to a real database or search engine. Focus on the decisions and data shapes a PHP application would use.

Show solution

This solution models the main decisions before any specific database or search SDK is introduced.

PHP example
<?php

declare(strict_types=1);

function normaliseSearchTerm(string $term): string
{
    return trim(preg_replace('/\s+/', ' ', $term) ?? '');
}

function searchStrategy(int $catalogueSize, bool $needsTypoTolerance, bool $needsFacets): string
{
    if ($catalogueSize <= 10_000 && !$needsTypoTolerance && !$needsFacets) {
        return 'database search';
    }

    return 'search engine';
}

function productSearchDocument(array $product): array
{
    return [
        'id' => (int) $product['id'],
        'name' => (string) $product['name'],
        'category' => (string) $product['category'],
        'status' => (string) $product['status'],
        'visible_to_user_ids' => array_map('intval', $product['visible_to_user_ids']),
    ];
}

function preserveSearchOrder(array $searchIds, array $databaseRowsById): array
{
    $ordered = [];

    foreach ($searchIds as $id) {
        if (isset($databaseRowsById[$id])) {
            $ordered[] = $databaseRowsById[$id];
        }
    }

    return $ordered;
}

$strategy = searchStrategy(2_500, false, false);
$term = normaliseSearchTerm('  desk    lamp  ');
$document = productSearchDocument([
    'id' => 42,
    'name' => 'Desk lamp',
    'category' => 'lighting',
    'status' => 'published',
    'visible_to_user_ids' => [7, 42],
]);

$orderedRows = preserveSearchOrder(
    [42, 10],
    [
        10 => ['id' => 10, 'name' => 'Office chair'],
        42 => ['id' => 42, 'name' => 'Desk lamp'],
    ]
);

echo $strategy . PHP_EOL;
echo $term . PHP_EOL;
echo $document['status'] . PHP_EOL;
echo implode(', ', array_column($orderedRows, 'name')) . PHP_EOL;

// Prints:
// database search
// desk lamp
// published
// Desk lamp, Office chair

The important point is that search has both a technical shape and a product shape. The application must choose the right search backend, index only safe fields, apply visibility rules, and preserve search result order when loading full records.