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
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
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
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
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
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
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
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
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.
Define Search Behavior Before Selecting A Product
Search requirements should be written in terms of user behavior: which fields are searchable, whether misspellings should match, how prefixes and phrases behave, which filters and facets are needed, how results are ranked, and how quickly updates must become visible. A product name lookup with two filters is a different problem from multilingual document search with synonyms, highlighting, and merchandising rules.
Start with the database when its indexes and full-text features satisfy the requirement at acceptable latency. This keeps authorization, transactions, backups, and schema ownership in one system. Move to a dedicated engine when measured query needs justify a denormalized index and the team can operate the synchronization path.
Treat The Index As A Projection
A search document is shaped for retrieval, not copied mechanically from normalized tables. It may combine a product name, category labels, searchable attributes, popularity signals, and a public availability flag. Record where every field comes from and how a full rebuild derives it from authoritative data.
Updates can be synchronous or asynchronous. Synchronous indexing lengthens the write path and still cannot create one atomic transaction across the database and search service. Asynchronous indexing through an outbox or queue accepts visible lag but provides a replayable path. Define an indexing-lag objective and expose enough version information to detect documents that fall behind.
Every index needs a deterministic full rebuild. Create a new versioned index or collection, load it from the source of truth, compare counts and representative queries, then switch an alias. Keep the previous index until the cutover is verified. Mutating the only index in place makes mapping changes and rollback unnecessarily dangerous.
Relevance Needs Regression Tests
A search endpoint can return HTTP 200 and still regress badly. Maintain a small judged query set containing important terms, misspellings, category filters, empty queries, rare products, and queries with no good result. Record the expected top results or relevance properties. Run the corpus when analyzers, synonyms, boosts, document fields, or engine versions change.
Do not optimize only for one aggregate score. Merchandising rules, diversity, freshness, and exact identifier matches may be product requirements. Keep manual overrides explicit and time-bounded so they do not silently dominate the ranking months later.
Pagination also affects quality. Deep offset pagination can become expensive and unstable while documents change. Use cursor or search-after mechanisms for long result sets, and define whether a user is expected to see a stable snapshot or the latest index state.
Authorization Must Survive Denormalization
Index only fields needed for search and result display. Search clusters are additional copies of data with their own credentials, logs, snapshots, and retention rules. Sensitive internal notes should not be indexed merely because the source row contains them.
Tenant and visibility filters must be applied by trusted server-side code. Do not accept a client-supplied tenant identifier as authority. For sensitive resources, retrieve candidate identifiers from search and re-check authorization against authoritative application data before returning details. This costs another lookup but protects against stale visibility fields and indexing defects.
Test cross-tenant and unpublished records using an untrusted client path. A query that correctly ranks permitted results is not sufficient if a crafted filter can reveal another tenant's document count, facet values, or highlighted text.
Compare Engines On The Whole Lifecycle
Meilisearch emphasizes approachable setup and product-search features. Elasticsearch and OpenSearch expose broad distributed search and analytics capabilities with significant mapping and cluster decisions. Database full-text search may remain the best operational fit for modest workloads. The correct choice depends on language support, relevance control, filtering, consistency needs, client maturity, managed-service options, upgrade path, and team experience.
Benchmark with representative document counts, field sizes, filters, and concurrent queries. Include indexing throughput and rebuild time, not only median query latency. Record p95 and p99 latency, rejected operations, disk growth, memory pressure, and behavior during node loss or deployment.
Operational Verification
Monitor indexing lag, failed updates, queue depth, document-count differences, query latency, zero-result rate, and cluster health. Alerting on cluster color alone misses stale or semantically wrong documents. Provide a diagnostic that compares an authoritative record version with the indexed version for one identifier.
Restore snapshots or backups into an isolated environment and run the judged query corpus. A backup is useful only if the team can restore a compatible cluster and recover aliases, mappings, analyzers, and application credentials.
Exercise an engine outage. Define whether the application returns a clear temporary failure, falls back to a simpler database query, or serves a limited cached experience. A fallback must be intentionally bounded; sending unrestricted search traffic to an unprepared database can turn one outage into two.
After this lesson, you should be able to define search requirements independently of a product, decide when database search is sufficient, model a search index as a rebuildable projection, test relevance and authorization, compare engines across their operational lifecycle, and verify synchronization and recovery rather than trusting successful queries alone.
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
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.