Search System Overview
Core Components
Search System:
├── Indexing Pipeline
│ ├── Data extraction
│ ├── Transformation
│ ├── Enrichment
│ └── Indexing
├── Query Processing
│ ├── Query parsing
│ ├── Query expansion
│ ├── Filtering
│ └── Sorting
├── Relevance Engine
│ ├── Text relevance
│ ├── Business rules
│ ├── Personalization
│ └── Learning
└── Response Generation
├── Results formatting
├── Facets calculation
├── Highlighting
└── Suggestions
Index Structure
{
"mappings": {
"properties": {
"entity_id": { "type": "integer" },
"sku": { "type": "keyword" },
"name": {
"type": "text",
"analyzer": "standard",
"fields": {
"keyword": { "type": "keyword" },
"autocomplete": {
"type": "text",
"analyzer": "autocomplete"
}
}
},
"description": { "type": "text", "analyzer": "html_strip" },
"price": { "type": "float" },
"qty": { "type": "integer" },
"category_ids": { "type": "keyword" },
"in_stock": { "type": "boolean" },
"created_at": { "type": "date" },
"updated_at": { "type": "date" },
"attributes": {
"type": "object",
"dynamic": true
}
}
}
}
Indexing Strategies
Full Reindex
// Full catalog reindex
public function reindexAll()
{
$products = $this->productCollection->create()
->addAttributeToSelect('*')
->load();
$batch = [];
foreach ($products as $product) {
$batch[] = $this->prepareIndexData($product);
if (count($batch) >= 1000) {
$this->bulkIndex($batch);
$batch = [];
}
}
if (!empty($batch)) {
$this->bulkIndex($batch);
}
}
private function prepareIndexData($product)
{
return [
'entity_id' => $product->getId(),
'sku' => $product->getSku(),
'name' => $product->getName(),
'description' => strip_tags($product->getDescription()),
'price' => (float) $product->getPrice(),
'qty' => (int) $product->getStockItem()->getQty(),
'category_ids' => $product->getCategoryIds(),
'in_stock' => $product->isSalable(),
'created_at' => $product->getCreatedAt(),
'updated_at' => $product->getUpdatedAt(),
];
}
Incremental Indexing
// On product save
public function onProductSave($observer)
{
$product = $observer->getEvent()->getProduct();
$indexData = $this->prepareIndexData($product);
$this->searchClient->index([
'index' => 'catalog_product',
'id' => $product->getId(),
'body' => $indexData
]);
}
// On product delete
public function onProductDelete($observer)
{
$productId = $observer->getEvent()->getProductId();
$this->searchClient->delete([
'index' => 'catalog_product',
'id' => $productId
]);
}
Index Management
// Index aliases
public function switchIndex($newIndex)
{
$oldIndex = $this->getIndexAlias('catalog_product');
// Update alias
$this->searchClient->indices()->updateAliases([
'body' => [
'actions' => [
['remove' => ['index' => $oldIndex, 'alias' => 'catalog_product']]],
['add' => ['index' => $newIndex, 'alias' => 'catalog_product']]
]
]
]);
// Delete old index
$this->searchClient->indices()->delete(['index' => $oldIndex]);
}
Relevance Tuning
Text Relevance
{
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "wireless headphones",
"fields": [
"name^5",
"sku^3",
"description^1",
"categories^2"
],
"type": "best_fields",
"fuzziness": "AUTO"
}
}
],
"should": [
{
"term": {
"in_stock": {
"value": true,
"boost": 10
}
}
},
{
"range": {
"price": {
"gte": 50,
"lte": 200,
"boost": 5
}
}
}
]
}
}
}
Boosting Strategies
Field boosting:
├── name: ^5 (highest priority)
├── sku: ^3 (exact match)
├── categories: ^2 (context)
└── description: ^1 (full text)
Business rules:
├── In-stock products: +10 boost
├── Sale products: +5 boost
├── New products: +3 boost
└── High-rated products: +2 boost
Personalization:
├── Viewed products: +5 boost
├── Purchased categories: +3 boost
└── Customer segment: +2 boost
Relevance Testing
// Test relevance
public function testRelevance($query, $expectedResults)
{
$results = $this->searchService->search($query);
$actualIds = array_column($results['hits'], 'entity_id');
$precision = count(array_intersect($actualIds, $expectedResults)) / count($actualIds);
$recall = count(array_intersect($actualIds, $expectedResults)) / count($expectedResults);
return [
'precision' => $precision,
'recall' => $recall,
'f1' => 2 * ($precision * $recall) / ($precision + $recall)
];
}
Faceted Search
Facet Configuration
// Facet definition
$facets = [
'category' => [
'type' => 'terms',
'field' => 'category_ids',
'name' => 'Categories',
'size' => 20
],
'price' => [
'type' => 'range',
'field' => 'price',
'name' => 'Price',
'ranges' => [
['from' => 0, 'to' => 50],
['from' => 50, 'to' => 100],
['from' => 100, 'to' => 200],
['from' => 200]
]
],
'in_stock' => [
'type' => 'terms',
'field' => 'in_stock',
'name' => 'Availability'
]
];
Facet Query
{
"query": {
"bool": {
"must": [{ "match": { "name": "headphones" } }],
"filter": [
{ "terms": { "category_ids": ["5"] } },
{ "range": { "price": { "gte": 50, "lte": 200 } } }
]
}
},
"aggs": {
"categories": {
"terms": { "field": "category_ids", "size": 20 }
},
"price_ranges": {
"range": {
"field": "price",
"ranges": [
{ "key": "0-50", "to": 50 },
{ "key": "50-100", "from": 50, "to": 100 },
{ "key": "100-200", "from": 100, "to": 200 },
{ "key": "200+", "from": 200 }
]
}
},
"in_stock": {
"filter": { "term": { "in_stock": true } },
"aggs": {
"count": { "value_count": { "field": "entity_id" } }
}
}
}
}
Facet Display
// Process facets for display
public function formatFacets($aggregations)
{
$facets = [];
foreach ($aggregations as $key => $agg) {
$facet = [
'name' => $key,
'label' => $this->getFacetLabel($key),
'options' => []
];
if (isset($agg['buckets'])) {
foreach ($agg['buckets'] as $bucket) {
$facet['options'][] = [
'value' => $bucket['key'],
'label' => $this->getFacetOptionLabel($key, $bucket['key']),
'count' => $bucket['doc_count']
];
}
}
$facets[] = $facet;
}
return $facets;
}
Autocomplete Design
Autocomplete Query
{
"suggest": {
"product-suggest": {
"prefix": "wire",
"completion": {
"field": "name.autocomplete",
"fuzzy": {
"fuzziness": "AUTO"
},
"size": 10
}
}
}
}
Autocomplete Implementation
// Autocomplete service
public function autocomplete($query, $limit = 10)
{
$results = $this->searchClient->search([
'index' => 'catalog_product',
'body' => [
'suggest' => [
'product-suggest' => [
'prefix' => $query,
'completion' => [
'field' => 'name.autocomplete',
'size' => $limit,
'fuzzy' => ['fuzziness' => 'AUTO']
]
]
]
]
]);
$suggestions = [];
foreach ($results['suggest']['product-suggest'][0]['options'] as $option) {
$suggestions[] = [
'text' => $option['text'],
'score' => $option['_score'],
'product_id' => $option['_source']['entity_id'],
'sku' => $option['_source']['sku'],
'price' => $option['_source']['price']
];
}
return $suggestions;
}
// Search suggestions with highlighting
public function searchSuggestions($query)
{
$results = $this->searchClient->search([
'index' => 'catalog_product',
'body' => [
'query' => [
'multi_match' => [
'query' => $query,
'fields' => ['name^5', 'sku^3'],
'type' => 'bool_prefix'
]
],
'highlight' => [
'fields' => [
'name' => ['number_of_fragments' => 0]
]
],
'size' => 5
]
]);
return $results;
}
Practice Problems
Design search system for 500K products with autocomplete, faceted search, and personalization.
Solution
// System:
// 1. Index: Full + incremental, batch 1000
// 2. Relevance: name^5, sku^3, business rules
// 3. Facets: Category, price, availability
// 4. Autocomplete: Prefix + fuzzy, cached
// 5. Personalization: Boost viewed/purchased
// 6. Performance: Search cache, result caching Quiz
1. What is the purpose of field boosting?
2. What is faceted search?
3. How does autocomplete work?
4. What is the benefit of incremental indexing?
Flashcards
Question
Search field weights?
Click to reveal answer
Answer
name^5, sku^3, categories^2, description^1
Question
Faceted search purpose?
Click to reveal answer
Answer
Filtering with result counts for each option
Question
Autocomplete technique?
Click to reveal answer
Answer
Prefix matching with fuzzy tolerance
Question
Incremental indexing?
Click to reveal answer
Answer
Update only changed products in real-time
Question
Relevance metrics?
Click to reveal answer
Answer
Precision, Recall, F1 score
Revision Notes
Key Takeaways
- 1. Field boosting: name^5, sku^3, categories^2, description^1
- 2. Faceted search: Filtering with aggregation counts
- 3. Autocomplete: Prefix + fuzzy matching for suggestions
- 4. Incremental indexing: Real-time updates without full reindex
- 5. Relevance: Measure with precision, recall, F1
Interview Tips
- • Explain relevance tuning strategies
- • Discuss faceted search implementation
- • Know autocomplete techniques
- • Understand indexing strategies
Cheat Sheet
Search System
- Boost: name^5, sku^3, categories^2, desc^1
- Facets: Filter + count aggregation
- Autocomplete: Prefix + fuzzy
- Indexing: Full + incremental
- Relevance: Precision, Recall, F1