lindemannrock / craft-search-manager
Advanced multi-backend search management for Craft CMS - supports Algolia, File, Meilisearch, MySQL, Redis, and Typesense
Installs: 52
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
Type:craft-plugin
pkg:composer/lindemannrock/craft-search-manager
Requires
- php: ^8.2
- algolia/algoliasearch-client-php: ^4.0
- craftcms/cms: ^5.0.0
- lindemannrock/craft-logging-library: ^5.0
- matomo/device-detector: ^6.4
- meilisearch/meilisearch-php: ^1.0
- typesense/typesense-php: ^4.0
Requires (Dev)
- craftcms/ecs: dev-main
- craftcms/phpstan: dev-main
- phpunit/phpunit: ^10.5
This package is auto-updated.
Last update: 2025-12-20 17:41:56 UTC
README
Advanced multi-backend search management for Craft CMS - supports Algolia, File, Meilisearch, MySQL, PostgreSQL, Redis, and Typesense.
Features
Multi-Backend Support
- Algolia - Cloud-hosted search service (Scout replacement)
- File - Local file storage in
@storage/runtime/search-manager/indices/(no external dependencies) - Meilisearch - Self-hosted, open-source alternative to Algolia
- MySQL - Built-in BM25 search using Craft's MySQL database
- PostgreSQL - Built-in BM25 search using Craft's PostgreSQL database
- Redis - Fast in-memory BM25 search with persistence (can reuse Craft's Redis cache)
- Typesense - Open-source search engine with typo tolerance
Advanced Search Features (MySQL, PostgreSQL, Redis, File)
Search Operators:
- Phrase Search -
"exact phrase"for sequential word matching - NOT Operator -
test NOT spamto exclude terms - Field-Specific -
title:blogorcontent:testto search specific fields - Wildcards -
test*matches test, tests, testing, tested - Per-term Boosting -
test^2 entryto weight specific terms - Boolean Operators -
test OR entryandtest AND entry
Ranking & Relevance:
- BM25 Ranking Algorithm - Industry-standard relevance scoring
- Fuzzy Matching - Typo tolerance with n-gram similarity (finds "test" when searching "tst")
- Title Boosting - Results with query terms in titles rank 5x higher (configurable)
- Phrase Boosting - Exact phrase matches rank 4x higher (configurable)
- Exact Match Boosting - Documents matching all terms rank 3x higher (configurable)
- Stop Words Filtering - Automatic removal of common words (the, a, is, etc.)
UX Features:
- Highlighting - Highlight matched terms with
<mark>tags (configurable) - Context Snippets - Show excerpts around matched terms
- Autocomplete - Search-as-you-type suggestions based on indexed terms
Multi-Language:
- 5 Languages Supported - English, Arabic, German, French, Spanish stop words
- Localized Boolean Operators - AND/OR/NOT in all 5 languages (UND/ODER/NICHT, ET/OU/SAUF, etc.)
- Auto-Detection - Language detected from element's site automatically
- Regional Variants - Support for ar-SA (Saudi), ar-EG (Egypt), fr-CA (Quebec), etc.
- Language Filtering - Filter results by language for multi-site indices
- API Language Override - Mobile apps can specify language for localized operators
Comprehensive Analytics
- Search Tracking - Track every search query with hits count and execution time
- Query Rules Tracking - Track which rules fire, how often, and their effectiveness
- Promotions Tracking - Track promotion impressions, positions, and triggering queries
- Synonyms Tracking - Track when synonym expansion is used
- Source Detection - Auto-detect search origin (frontend, CP, API) or pass custom sources
- Platform & App Tracking - Track platform (iOS 17, Android 14) and app version for mobile apps
- Device Detection - Powered by Matomo DeviceDetector for accurate device, browser, and OS identification
- Geographic Detection - Track visitor location (country, city, region) via ip-api.com
- Bot Filtering - Identify and filter bot traffic (GoogleBot, BingBot, etc.)
- Zero-Hit Tracking - Identify queries that return no results (content gaps)
- Performance Metrics - Dedicated Performance tab with cache hit rate, response time trends, fastest/slowest queries
- Intent & Source Charts - Visual breakdown of search intent and source distribution
- Privacy-First - IP hashing with salt, optional subnet masking, GDPR-friendly
- Referrer Tracking - See where search traffic is coming from
- Export Options - CSV and JSON export with clean column names (Hits, Synonyms, Rules, Promotions, Redirected)
- Automatic Cleanup - Configurable retention period (0-3650 days)
Analytics Tabs:
- Overview - Summary stats, search trends, intent/source breakdown
- Recent Searches - Detailed log with hits, synonyms, rules, promotions columns
- Query Rules - Top triggered rules, rules by action type, triggering queries (only shown if rules exist)
- Promotions - Top promoted elements, impressions by position, triggering queries (only shown if promotions exist)
- Content Gaps - Zero-hit clusters and recent failed queries
- Performance - Cache stats, response times, fastest/slowest queries
- Traffic & Devices - Device, browser, OS breakdown, peak hours
- Geographic - Country and city breakdown (when geo detection enabled)
Performance Caching
- Search Results Cache - Cache search results to reduce backend load and improve response times
- Device Detection Cache - Cache parsed user-agent strings to avoid re-parsing
- Popular Queries Only - Only cache frequently-searched queries to save storage space
- Configurable Durations - Set cache TTL per cache type (default: 1 hour)
- Cache Management - Clear caches via Control Panel utilities or Craft's Clear Caches
- Craft Integration - Search caches available in Craft's Clear Caches utility (safe, auto-regenerate)
- Storage Locations:
- Device cache:
@storage/runtime/search-manager/cache/device/ - Search cache:
@storage/runtime/search-manager/cache/search/
- Device cache:
Automatic Indexing
- Auto-index elements when saved (configurable)
- Queue-based batch indexing for better performance
- Manual rebuild via Control Panel or CLI
- Element deletion automatically removes from index
Native Search Replacement
- Replace Craft's search service - Optional setting to replace
Craft::$app->search - CP search integration - Control Panel searches use your backend
- Template compatibility -
Entry::find()->search('query')uses your backend - Seamless fallback - Falls back to Craft's search if no index configured
- Built-in backends only - Works with MySQL, PostgreSQL, Redis, and File backends
Custom Transformers
- Transform elements into searchable documents
- Scout-compatible transformer API for easy migration
- Built-in transformers for entries, assets, categories
- Custom transformers per element type, site, or section
- Priority-based transformer resolution
Promotions (Pinned Results)
- Pin Elements - Force specific elements to fixed positions in search results
- Match Types - Exact match, contains, or prefix matching for query patterns
- Position Control - Specify exact position (1st, 2nd, 3rd, etc.)
- Scope Control - Apply to specific indices and/or sites
- Enable/Disable - Toggle promotions without deleting
- Bulk Actions - Enable, disable, or delete multiple promotions at once
- Per-Site Status - Respects element status per site (disabled/pending/expired elements excluded from that site's results)
Query Rules
- Synonyms - Expand searches to include related terms (e.g., "laptop" → "notebook, computer")
- Section Boosting - Boost results from specific sections by multiplier
- Category Boosting - Boost results in specific categories
- Element Boosting - Boost specific elements by ID
- Result Filtering - Filter results by field values when query matches
- Query Redirects - Redirect users to a URL instead of showing results
- Match Types - Exact, contains, prefix, or regex pattern matching
- Priority System - Higher priority rules applied first
- Global or Index-Specific - Apply rules to all indices or specific ones
Control Panel Interface
- Full CP section for managing indices
- Promotions management with filtering and bulk actions
- Query Rules management with action type configuration
- Create, edit, delete, rebuild indices
- Backend status monitoring
- Analytics dashboard
- Comprehensive settings with config override warnings
- Test Search - Test searches across all sites with element type and site info per result
Developer-Friendly
- Console commands for all operations
- Event system for before/after indexing hooks
- Template variables for frontend search
- Multi-site support
- Database-backed settings (not project config)
- Config file override layer
Requirements
- PHP 8.2+
- Craft CMS 5.0+
- LindemannRock Logging Library ^5.0 (installed automatically)
- matomo/device-detector ^6.4 (installed automatically for analytics)
Optional Backend Requirements
- Algolia: PHP cURL extension
- Meilisearch: Meilisearch server running
- Redis: PHP Redis extension
- Typesense: Typesense server running
Installation
Via Composer
cd /path/to/project
composer require lindemannrock/craft-search-manager
./craft plugin/install search-manager
Using DDEV
cd /path/to/project
ddev composer require lindemannrock/craft-search-manager
ddev craft plugin/install search-manager
Via Control Panel
In the Control Panel, go to Settings → Plugins and click "Install" for Search Manager.
⚠️ Required Post-Install Step
IMPORTANT: After installation, you MUST generate the IP hash salt for analytics to work:
php craft search-manager/security/generate-salt
Or with DDEV:
ddev craft search-manager/security/generate-salt
What happens if you skip this:
- ❌ Analytics tracking will fail with error:
IP hash salt not configured - ❌ Search will still work, but won't track queries
- ✅ You can generate the salt later, but no analytics will be collected until you do
Quick Start:
# After plugin installation: php craft search-manager/security/generate-salt # The command will automatically add SEARCH_MANAGER_IP_SALT to your .env file # Copy this value to staging/production .env files manually
Optional: Copy Config File
cp vendor/lindemannrock/craft-search-manager/src/config.php config/search-manager.php
Important: IP Privacy Protection
Search Manager uses privacy-focused IP hashing with a secure salt:
- ✅ Rainbow-table proof - Salted SHA256 prevents pre-computed attacks
- ✅ Unique visitor tracking - Same IP = same hash
- ✅ Geo-location preserved - Country/city extracted BEFORE hashing
- ✅ Maximum privacy - Original IPs never stored, unrecoverable
Setup Instructions:
- Generate salt:
php craft search-manager/security/generate-salt - Command automatically adds
SEARCH_MANAGER_IP_SALTto your.envfile - Manually copy the salt value to staging/production
.envfiles - Never regenerate the salt in production
How It Works:
- Plugin automatically reads salt from
.env(no config file needed!) - Config file can override if needed:
'ipHashSalt' => App::env('SEARCH_MANAGER_IP_SALT') - If no salt found, error banner shown in settings
Security Notes:
- Never commit the salt to version control
- Store salt securely (password manager recommended)
- Use the SAME salt across all environments (dev, staging, production)
- Changing the salt will break unique visitor tracking history
Local Development: Analytics Location Override
When running locally (DDEV, localhost), analytics will default to Dubai, UAE because local IPs can't be geolocated. To set your actual location for testing:
Option 1: Config File (recommended for project-wide default)
// config/search-manager.php return [ 'defaultCountry' => 'US', 'defaultCity' => 'New York', ];
Option 2: Environment Variable (recommended for per-environment control)
# .env SEARCH_MANAGER_DEFAULT_COUNTRY=US SEARCH_MANAGER_DEFAULT_CITY="New York"
Fallback Priority:
- Config file setting
- .env variable
- Hardcoded default: Dubai, UAE
Supported locations:
- US: New York, Los Angeles, Chicago, San Francisco
- GB: London, Manchester
- AE: Dubai, Abu Dhabi (default: Dubai)
- SA: Riyadh, Jeddah
- DE: Berlin, Munich
- FR: Paris
- CA: Toronto, Vancouver
- AU: Sydney, Melbourne
- JP: Tokyo
- SG: Singapore
- IN: Mumbai, Delhi
Important: This setting is safe to use in all environments (dev, staging, production). It only affects private/local IP addresses (127.0.0.1, 192.168.x.x, 10.x.x.x, etc.). Real visitor IPs in production will always use actual geolocation from ip-api.com. This means you can safely commit config file settings without impacting production analytics.
Quick Start
1. Configure Backend
Create config/search-manager.php:
<?php use craft\helpers\App; return [ '*' => [ 'searchBackend' => 'meilisearch', 'backends' => [ 'meilisearch' => [ 'enabled' => true, 'host' => App::env('MEILISEARCH_HOST'), 'apiKey' => App::env('MEILISEARCH_API_KEY'), ], ], ], ];
2. Define Indices
Add indices to config/search-manager.php:
'indices' => [ 'entries-en' => [ 'name' => 'Entries (English)', 'elementType' => \craft\elements\Entry::class, 'siteId' => 1, 'criteria' => function($query) { return $query->section(['news', 'blog']); }, 'transformer' => \modules\transformers\EntryTransformer::class, 'enabled' => true, ], ],
3. Create a Transformer
<?php namespace modules\transformers; use craft\base\ElementInterface; use craft\elements\Entry; use lindemannrock\searchmanager\transformers\BaseTransformer; class EntryTransformer extends BaseTransformer { protected function getElementType(): string { return Entry::class; } public function transform(ElementInterface $element): array { $data = $this->getCommonData($element); $data['content'] = $this->stripHtml($element->body); $data['excerpt'] = $this->getExcerpt($element->body, 200); $data['section'] = $element->section->handle; return $data; } }
4. Rebuild Indices
php craft search-manager/index/rebuild
Usage
Using Native Search Replacement (Automatic)
⚠️ Note: Native search replacement only works with MySQL, PostgreSQL, Redis, and File backends. Not available for Algolia, Meilisearch, or Typesense.
Enable in settings (CP → Search Manager → Settings → Indexing) or config:
'replaceNativeSearch' => true,
What This Does:
- ✅ Control Panel searches use your backend (Entries → Search, Assets → Search, etc.)
- ✅ Template searches use your backend automatically
- ✅ Element queries use your backend (
Entry::find()->search()) - ✅ All search operators work in CP search boxes!
Usage:
{# In templates - automatically uses your backend #} {% set entries = craft.entries.search('my query').all() %} {# Advanced operators work in CP and templates! #} {% set entries = craft.entries.search('"craft cms" NOT plugin').all() %} {% set entries = craft.entries.search('title:tutorial test*').all() %}
In Control Panel: When enabled, you can use advanced operators directly in CP search boxes:
- Type:
"exact phrase"in Entries search → Phrase search works! - Type:
craft NOT plugin→ Exclusion works! - Type:
title:blog→ Field-specific search works! - Type:
test*→ Wildcards work!
All features available everywhere!
Multi-Index Search
Search across multiple indices at once and get merged, scored results:
{# Search across multiple indices #} {% set results = craft.searchManager.searchMultiple(['products', 'blog', 'pages'], 'search query') %} {# Total results across all indices #} <p>Found {{ results.total }} results</p> {# Per-index breakdown #} <ul> {% for indexName, count in results.indices %} <li>{{ indexName }}: {{ count }} results</li> {% endfor %} </ul> {# Loop through merged results (sorted by score) #} {% for hit in results.hits %} {% set element = craft.entries.id(hit.objectID).one() %} <div class="result result--{{ hit._index }}"> <h3>{{ element.title }}</h3> <span class="source">From: {{ hit._index }}</span> <span class="score">Score: {{ hit.score|number_format(2) }}</span> </div> {% endfor %}
Return Structure:
[
'hits' => [
['objectID' => 123, 'score' => 45.2, '_index' => 'products'],
['objectID' => 456, 'score' => 38.1, '_index' => 'blog'],
// ... merged and sorted by score
],
'total' => 150, // Sum across all indices
'indices' => [ // Per-index breakdown
'products' => 50,
'blog' => 100,
],
]
Features:
- ✅ Results merged and sorted by relevance score
- ✅ Each hit tagged with
_indexfor source identification - ✅ Per-index result counts for faceted display
- ✅ Respects current site context automatically
- ✅ Cache-aware (per-index, per-site caching)
Using Search Manager Directly (Explicit)
{# Basic search #} {% set results = craft.searchManager.search('entries-en', 'search query') %} {% for hit in results.hits %} <h3>{{ hit.title }}</h3> <p>{{ hit.excerpt }}</p> <p>Relevance Score: {{ hit.score }}</p> <a href="{{ hit.url }}">Read more</a> {% endfor %} <p>Total results: {{ results.total }}</p> {# Search with boolean operators #} {% set orResults = craft.searchManager.search('entries-en', 'test OR entry') %} {% set andResults = craft.searchManager.search('entries-en', 'test AND entry') %} {# Fuzzy search (typo tolerance) #} {% set fuzzyResults = craft.searchManager.search('entries-en', 'tst') %} {# Will find documents containing "test" #}
Search Operators (MySQL, PostgreSQL, Redis, File)
Search Manager supports powerful query operators for precise search control:
1. Phrase Search (Exact Sequences)
{# Find exact phrase in sequence #} {% set results = craft.searchManager.search('entries', '"craft cms"') %} {# Only matches documents with "craft" followed by "cms" #} {# Ranks 4x higher than regular matches #}
2. NOT Operator (Exclusion)
{# Find "craft" but exclude documents with "plugin" #} {% set results = craft.searchManager.search('entries', 'craft NOT plugin') %} {# Combine with other operators #} {% set results = craft.searchManager.search('entries', '"craft cms" NOT plugin NOT theme') %}
3. Field-Specific Search
{# Search only in titles #} {% set results = craft.searchManager.search('entries', 'title:blog') %} {# Search only in content #} {% set results = craft.searchManager.search('entries', 'content:tutorial') %} {# Combine fields #} {% set results = craft.searchManager.search('entries', 'title:craft content:plugin') %}
4. Wildcard Search (Prefix Matching)
{# Find all words starting with "test" #} {% set results = craft.searchManager.search('entries', 'test*') %} {# Matches: test, tests, testing, tested, etc. #} {# Multiple wildcards #} {% set results = craft.searchManager.search('entries', 'test* OR craft*') %}
5. Per-Term Boosting
{# Boost "craft" more than "cms" #} {% set results = craft.searchManager.search('entries', 'craft^2 cms') %} {# Custom weights for multiple terms #} {% set results = craft.searchManager.search('entries', 'craft^3 plugin^2 tutorial^1.5') %}
6. Boolean Operators (AND/OR)
{# OR: Find docs with either term #} {% set results = craft.searchManager.search('entries', 'craft OR cms') %} {# AND: Find docs with both terms (default) #} {% set results = craft.searchManager.search('entries', 'craft AND cms') %} {% set results = craft.searchManager.search('entries', 'craft cms') %} {# Same as above #}
Localized Boolean Operators:
Operators work in 5 languages (case-insensitive). Language is auto-detected from site settings:
| Language | AND | OR | NOT |
|---|---|---|---|
| English | AND | OR | NOT |
| German | UND | ODER | NICHT |
| French | ET | OU | SAUF |
| Spanish | Y | O | NO |
| Arabic | و | أو / او | ليس / لا |
{# German site - both work #} {% set results = craft.searchManager.search('products', 'kaffee ODER tee') %} {% set results = craft.searchManager.search('products', 'kaffee OR tee') %} {# English fallback #} {# French site #} {% set results = craft.searchManager.search('products', 'café OU thé') %} {% set results = craft.searchManager.search('products', 'café SAUF décaféiné') %}
Note: English operators always work as fallback regardless of site language.
7. Combined Operators (Power Queries)
{# Complex query combining multiple operators #} {% set results = craft.searchManager.search('entries', 'craft* OR plugin title:tutorial NOT beginner "getting started"^2' ) %} {# Finds: - Words starting with "craft" OR "plugin" - Must have "tutorial" in title - Excludes "beginner" - Boosts exact phrase "getting started" 2x #}
Fuzzy Matching:
- Automatically finds similar terms using n-gram similarity
- Configurable similarity threshold (default: 0.50)
- Works with typos, missing letters, transpositions
- Examples: "tst" finds "test", "craaft" finds "craft"
Ranking Priority (Highest to Lowest):
- Phrase matches (
"exact phrase") - 4x boost - Title matches - 5x boost
- Exact matches (all terms present) - 3x boost
- Per-term boosts (
term^2) - custom multiplier - Single term matches - base BM25 score
Caching
Search Manager includes two caching layers for optimal performance:
Search Results Cache:
- Caches search results to avoid repeated backend queries
- Reduces API costs for external services (Algolia, Meilisearch, Typesense)
- Improves response times for all backends
- Optional "Popular Queries Only" mode - only cache queries searched ≥ N times
- Configurable cache duration (default: 1 hour)
- Storage options:
- File (default):
@storage/runtime/search-manager/cache/search/ - Redis: Uses Craft's Redis cache (recommended for edge networks)
- File (default):
Device Detection Cache:
- Caches parsed user-agent strings (device, browser, OS info)
- Powered by Matomo DeviceDetector library
- Prevents re-parsing the same user-agent repeatedly
- Configurable cache duration (default: 1 hour)
- Stored in:
@storage/runtime/search-manager/cache/device/
Configuration:
// config/search-manager.php return [ // Search results caching 'enableCache' => true, 'cacheStorageMethod' => 'file', // 'file' or 'redis' (use 'redis' for edge networks) 'cacheDuration' => 3600, // 1 hour 'cachePopularQueriesOnly' => false, 'popularQueryThreshold' => 5, // Cache after 5 searches // Device detection caching 'cacheDeviceDetection' => true, 'deviceDetectionCacheDuration' => 3600, // 1 hour ];
When to Use Redis Cache:
- ✅ Edge networks (Servd, Platform.sh, AWS with ElastiCache)
- ✅ Multi-server setups (shared cache across servers)
- ✅ High traffic sites (faster than file I/O)
- ✅ When Craft already uses Redis cache (reuses connection)
When to Use File Cache:
- ✅ Single-server setups
- ✅ Shared hosting without Redis
- ✅ Development environments
- ✅ Simple deployments
Popular Queries Example:
Query: "craft cms"
Search #1-4: Not cached (below threshold)
Search #5: Cached! (threshold met)
Search #6+: Served from cache (5ms vs 150ms)
Benefits:
- Faster response times (5-10ms vs 50-200ms)
- Reduced API costs (Algolia, Meilisearch, Typesense)
- Lower backend load (MySQL, Redis queries)
- Smart storage (popular queries only option)
Highlighting & Snippets
Highlight matched search terms and show contextual excerpts:
{# Basic highlighting #} {% set results = craft.searchManager.search('entries', 'craft cms') %} {% for hit in results.hits %} {% set element = craft.entries.id(hit.objectID).one() %} <h2>{{ craft.searchManager.highlight(element.title, 'craft cms')|raw }}</h2> {# Output: <h2>This is about <mark>craft</mark> <mark>cms</mark></h2> #} {% endfor %} {# Generate context snippets #} {% set snippets = craft.searchManager.snippets(element.body, 'craft cms', { snippetLength: 200, maxSnippets: 3 }) %} {% for snippet in snippets %} <p>{{ snippet|raw }}</p> {# Output: "...tutorial about <mark>craft</mark> <mark>cms</mark> development..." #} {% endfor %} {# Custom highlighting options #} {{ craft.searchManager.highlight(text, query, { tag: 'em', class: 'search-highlight', stripTags: true })|raw }}
Configuration:
// config/search-manager.php return [ 'enableHighlighting' => true, 'highlightTag' => 'mark', // HTML tag (mark, em, strong, span) 'highlightClass' => 'search-highlight', // Optional CSS class 'snippetLength' => 200, // Characters per snippet 'maxSnippets' => 3, // Max snippets per result ];
CSS Styling:
mark { background-color: #ffeb3b; padding: 2px 4px; border-radius: 2px; } /* Or with custom class */ .search-highlight { background-color: #ff9800; color: #fff; }
Autocomplete / Suggestions
Provide search-as-you-type suggestions based on indexed terms:
{# Basic autocomplete #} {% set suggestions = craft.searchManager.suggest('cra', 'entries') %} {# Returns: ['craft', 'craftcms', 'create'] #} {% for suggestion in suggestions %} <a href="?q={{ suggestion }}">{{ suggestion }}</a> {% endfor %} {# With options #} {% set suggestions = craft.searchManager.suggest('te', 'entries', { limit: 5, // Max suggestions minLength: 2, // Min characters fuzzy: true, // Enable typo-tolerance language: 'en' // Filter by language }) %} {# AJAX endpoint example #} <input type="search" id="search-input"> <script> document.getElementById('search-input').addEventListener('input', async (e) => { const query = e.target.value; if (query.length < 2) return; const response = await fetch(`/actions/search-manager/api/autocomplete?q=${query}&index=entries&only=suggestions`); const suggestions = await response.json(); // Display suggestions... }); </script>
Configuration:
// config/search-manager.php return [ 'enableAutocomplete' => true, 'autocompleteMinLength' => 2, // Min chars before suggesting 'autocompleteLimit' => 10, // Max suggestions 'autocompleteFuzzy' => false, // Typo-tolerance (slower) ];
AJAX / API Endpoints
Build instant search interfaces with AJAX endpoints:
Autocomplete Endpoint:
// GET /actions/search-manager/api/autocomplete // Default - returns both suggestions and element results const response = await fetch('/actions/search-manager/api/autocomplete?q=test&index=all-sites&limit=10'); const data = await response.json(); // Returns: { // "suggestions": ["test", "testing", "tested"], // "results": [ // {"text": "Test Product", "type": "product", "id": 123}, // {"text": "Testing Guide", "type": "article", "id": 456} // ] // } // Only suggestions - returns term strings const response = await fetch('/actions/search-manager/api/autocomplete?q=test&index=all-sites&only=suggestions'); const suggestions = await response.json(); // Returns: ["test", "testing", "tested"] // Only results - returns element objects with type info const response = await fetch('/actions/search-manager/api/autocomplete?q=test&index=all-sites&only=results'); const results = await response.json(); // Returns: [ // {"text": "Test Product", "type": "product", "id": 123}, // {"text": "Testing Guide", "type": "article", "id": 456} // ] // Filter results by element type const response = await fetch('/actions/search-manager/api/autocomplete?q=test&index=all-sites&only=results&type=product'); // Returns only product results
Autocomplete API Parameters:
| Parameter | Default | Description |
|---|---|---|
q |
(required) | Search query |
index |
all-sites |
Index handle to search |
limit |
10 |
Maximum suggestions/results |
only |
(none) | Return only suggestions or results (default returns both) |
type |
(none) | Filter results by element type (only affects results) |
Autocomplete Response Formats:
Default (no only param):
{
"suggestions": ["test", "testing", "tested"],
"results": [
{"text": "Test Product", "type": "product", "id": 123},
{"text": "Test Category", "type": "category", "id": 45}
]
}
Only suggestions (only=suggestions):
["test", "testing", "tested"]
Only results (only=results):
[
{"text": "Test Product", "type": "product", "id": 123},
{"text": "Test Category", "type": "category", "id": 45},
{"text": "Testing Guide", "type": "article", "id": 789}
]
Element Type Detection:
The type field is automatically derived from the element's section handle (singularized):
| Section Handle | Type |
|---|---|
products |
product |
categories |
category |
stores |
store |
blog-posts |
blog-post |
For non-Entry elements:
- Craft Categories →
category - Assets →
asset - Users →
user - Tags →
tag
Multi-section indices work correctly:
// all-ar index with multiple sections 'criteria' => fn($q) => $q->section(['products', 'categories', 'stores']), // Each entry gets type from its own section: // - Entry from products → type: "product" // - Entry from stores → type: "store"
Override in custom transformer:
$data['elementType'] = 'custom-type';
Search Endpoint:
// GET /actions/search-manager/api/search const response = await fetch('/actions/search-manager/api/search?q=craft cms&index=all-sites&limit=20'); const results = await response.json(); // Returns: {hits: [{objectID: 123, id: 123, score: 45.2, type: "product"}, ...], total: 15} // Filter by element type const response = await fetch('/actions/search-manager/api/search?q=bread&index=all-sites&type=product,category'); // Returns only products and categories // Mobile app with localized operators (German) const response = await fetch('/actions/search-manager/api/search?q=kaffee+ODER+tee&index=products&language=de'); // German OR operator works!
Search API Parameters:
| Parameter | Default | Description |
|---|---|---|
q |
(required) | Search query |
index |
all-sites |
Index handle to search |
limit |
20 |
Maximum results (use 0 for unlimited) |
type |
(none) | Filter by element type (e.g., product, category, product,category) |
language |
(site default) | Language code for localized operators (en, de, fr, es, ar) |
source |
(auto-detected) | Analytics source identifier (e.g., ios-app, android-app) |
platform |
(none) | Platform info for analytics (e.g., iOS 17.2, Android 14) |
appVersion |
(none) | App version for analytics (e.g., 2.1.0) |
Example: Instant Search with Type Icons
<input type="search" id="instant-search" placeholder="Search..."> <div id="suggestions"></div> <div id="results"></div> <script> const input = document.getElementById('instant-search'); const suggestionsDiv = document.getElementById('suggestions'); let debounceTimer; // Type to icon mapping const typeIcons = { 'product': '📦', 'category': '🏷️', 'article': '📄', 'page': '📃', 'entry': '📝' }; input.addEventListener('input', (e) => { clearTimeout(debounceTimer); const query = e.target.value; if (query.length < 2) return; debounceTimer = setTimeout(async () => { // Fetch both suggestions and results in one call const response = await fetch( `/actions/search-manager/api/autocomplete?q=${query}&index=all-sites` ); const data = await response.json(); // Display suggestions with icons suggestionsDiv.innerHTML = data.results.map(s => ` <div class="suggestion" data-id="${s.id}"> <span class="icon">${typeIcons[s.type] || '📝'}</span> <span class="text">${s.text}</span> <span class="type">${s.type}</span> </div> `).join(''); // Fetch full search results const searchResponse = await fetch( `/actions/search-manager/api/search?q=${query}&index=all-sites` ); const results = await searchResponse.json(); displayResults(results.hits); }, 300); }); </script>
Features:
- ✅ Works with MySQL, PostgreSQL, Redis, and File backends
- ✅ Returns real indexed terms and search results
- ✅ Supports all search operators in queries
- ✅ Language-aware (auto-detects from current site)
- ✅ Respects all configured settings (min length, limits, etc.)
- ✅ Element type detection for rich UI (icons, filtering)
API Response Structure:
Search response:
{
"hits": [
{
"objectID": 123,
"id": 123,
"promoted": true,
"position": 1,
"score": null,
"type": "product",
"title": "Featured Product"
},
{
"objectID": 456,
"id": 456,
"score": 45.23,
"type": "product"
}
],
"total": 150,
"meta": {
"synonymsExpanded": true,
"expandedQueries": ["laptop", "notebook", "computer"],
"rulesMatched": [
{
"id": 5,
"name": "Laptop synonyms",
"actionType": "synonym",
"actionValue": ["notebook", "computer"]
}
],
"promotionsMatched": [
{
"id": 1,
"elementId": 123,
"position": 1
}
]
}
}
Hit Fields:
| Field | Type | Description |
|---|---|---|
objectID |
int | Element ID |
id |
int | Element ID (alias) |
score |
float|null | BM25 relevance score (null for promoted items) |
type |
string | Element type (product, category, entry, etc.) |
promoted |
bool | Present and true for promoted/pinned results |
position |
int | Position in results (for promoted items) |
title |
string | Element title (for promoted items) |
Default Limits:
- Search API: 20 results (use
limit=0for unlimited) - Suggest API: 10 suggestions
All search operators work:
- Phrase:
?q="exact phrase" - Boolean:
?q=coffee OR tea,?q=coffee NOT decaf - Localized Boolean:
?q=kaffee ODER tee&language=de(German) - Wildcards:
?q=coff* - Field-specific:
?q=title:muesli - Boosting:
?q=coffee^2 beans
Mobile App Example (German):
// iOS app searching in German const response = await fetch('/actions/search-manager/api/search?' + new URLSearchParams({ q: 'kaffee ODER tee NICHT entkoffeiniert', index: 'products', language: 'de', source: 'ios-app', platform: 'iOS 17.2', appVersion: '2.1.0' })); // Uses German operators: ODER (OR), NICHT (NOT)
⚠️ Note: Default API limit (20) is hardcoded. TODO: Make configurable via settings.
Analytics Source Detection
Search Manager automatically detects the source of search requests and tracks analytics accordingly.
Auto-Detection Logic:
- CP - Craft Control Panel requests (detected via
getIsCpRequest()) - Frontend - Referrer from same host (same-site search forms)
- API - No referrer or external referrer (direct API calls)
Custom Source Tracking:
For mobile apps or custom integrations, you can pass custom analytics data:
// PHP - Pass custom analytics options $results = SearchManager::$plugin->backend->search('products', 'shoes', [ 'siteId' => 1, 'source' => 'ios-app', // Custom source identifier 'platform' => 'iOS 17.2', // Platform/OS version 'appVersion' => '2.1.0', // Your app version ]); // Or via Twig {% set results = craft.searchManager.search('products', 'shoes', { source: 'android-app', platform: 'Android 14', appVersion: '1.5.2' }) %}
Example Source Values:
frontend- Website search (auto-detected)cp- Control Panel search (auto-detected)api- Direct API calls (auto-detected)ios-app- iOS mobile appandroid-app- Android mobile appmobile-web- Mobile web PWApartner-api- Third-party integrations
What Gets Tracked:
| Field | Auto-Detected | Can Override |
|---|---|---|
source |
Yes (frontend/cp/api) | Yes |
platform |
No | Yes |
appVersion |
No | Yes |
ip |
Yes | No (security) |
country/city |
Yes | No (security) |
device/browser/os |
Yes (from User-Agent) | No |
Analytics Dashboard:
The Recent Searches tab displays source information:
- Source type (Frontend, CP, API, or custom)
- Platform and app version (when provided)
- Device, browser, and OS details
CSV Export:
Exported analytics include Platform and App Version columns for detailed analysis.
Promotions (Pinned Results)
Promotions allow you to pin specific elements to fixed positions in search results, bypassing normal relevance scoring.
Use Cases:
- Feature a specific product when users search for a category
- Promote sale items for seasonal keywords
- Ensure important content appears first for specific queries
Creating Promotions (Control Panel):
- Go to Search Manager → Promotions
- Click "New Promotion"
- Configure:
- Title: Descriptive name for organization (e.g., "Holiday Sale Banner")
- Query Pattern: The search query to match. Use commas for multiple patterns:
- Single:
sale - Multi-language:
sale, تخفيض, soldes, angebot(EN, AR, FR, DE)
- Single:
- Match Type: How to match the query
- Exact: Query must exactly match one of the patterns
- Contains: Query must contain one of the patterns anywhere
- Prefix: Query must start with one of the patterns
- Promoted Element: Select the element to promote
- Position: Where to place it (1 = first, 2 = second, etc.)
- Index: All Indexes or a specific search index
- Site: All Sites or a specific site
Example Scenarios:
Query Pattern: "laptop"
Match Type: Exact
Promoted Element: "MacBook Pro 2024" (Entry #123)
Position: 1
Result: When user searches exactly "laptop", MacBook Pro appears first
Query Pattern: "sale"
Match Type: Contains
Promoted Element: "Black Friday Deals" (Entry #456)
Position: 1
Result: Any query containing "sale" (e.g., "laptop sale", "sale items")
shows Black Friday Deals first
Query Pattern: "sale, تخفيض, soldes, angebot"
Match Type: Exact
Promoted Element: "Holiday Sale Banner" (Entry #789)
Position: 1
Index: All Indexes
Site: All Sites
Result: One promotion works across all languages - matches "sale" (EN),
"تخفيض" (AR), "soldes" (FR), or "angebot" (DE)
Bulk Actions:
- Select multiple promotions using checkboxes
- Enable/disable or delete in bulk
- Filter by status or match type
Per-Site Element Status:
Promotions automatically respect element status on a per-site basis:
- If an element is disabled for Site 1 but enabled for Site 2, the promotion will only appear on Site 2
- Elements with pending or expired post dates are excluded
- Uses Craft's
status('live')to check all status conditions
Example:
- Product "Summer Sale" is linked to promotion for query "sale"
- Product is disabled for English site, enabled for French/Arabic sites
- English site searches: promotion NOT shown
- French site searches: promotion shown at position 1
API Response:
Promoted items appear in hits with promoted: true, position, and score: null. See the main API Response Structure for full details.
Query Rules
Query Rules modify search behavior when queries match specific patterns. They support synonyms, boosting, filtering, and redirects.
Creating Query Rules (Control Panel):
- Go to Search Manager → Query Rules
- Click "New Query Rule"
- Select action type and configure
Action Types:
1. Synonyms
Expand search queries to include related terms.
Name: Laptop Synonyms
Match Value: laptop
Match Type: Exact
Action: Synonyms
Terms: notebook, portable computer, macbook
Result: Searching "laptop" also finds results containing
"notebook", "portable computer", or "macbook"
2. Boost Section
Increase relevance score for results from a specific section.
Name: Boost News for Current Events
Match Value: election
Match Type: Contains
Action: Boost Section
Section: news
Multiplier: 2.0
Result: News articles rank 2x higher when query contains "election"
3. Boost Category
Increase relevance score for results in a specific category.
Name: Boost Electronics for Tech Queries
Match Value: tech
Match Type: Prefix
Action: Boost Category
Category: Electronics
Multiplier: 1.5
Result: Queries starting with "tech" boost Electronics category results 1.5x
4. Boost Element
Increase relevance score for a specific element.
Name: Boost FAQ for Help Queries
Match Value: help
Match Type: Contains
Action: Boost Element
Element ID: 789
Multiplier: 3.0
Result: FAQ page (ID 789) ranks 3x higher for queries containing "help"
5. Filter Results
Filter search results by field value when query matches.
Name: Filter to In-Stock Only
Match Value: buy
Match Type: Contains
Action: Filter
Field: inStock
Value: true
Result: Queries containing "buy" only show in-stock items
6. Redirect
Redirect users to a specific URL instead of showing search results.
Name: Contact Redirect
Match Value: contact us
Match Type: Exact
Action: Redirect
URL: /contact
Result: Searching exactly "contact us" redirects to /contact page
Priority System:
- Rules with higher priority numbers are applied first
- Use priority to control rule order when multiple rules match
- Default priority is 0
Scope:
- Index: Apply to all indices (leave blank) or a specific index
- Site: Apply to all sites (leave blank) or a specific site
API Response Metadata:
When query rules are applied, they appear in the meta.rulesMatched array:
{
"hits": [...],
"meta": {
"rulesMatched": [
{
"id": 5,
"name": "Boost Electronics",
"actionType": "boost_element",
"actionValue": {"elementId": 123, "multiplier": 2.0}
}
]
}
}
The actionValue format varies by action type:
- boost_element:
{"elementId": 123, "multiplier": 2.0} - boost_section:
{"sectionHandle": "products", "multiplier": 2.0} - boost_category:
{"categoryId": 5, "multiplier": 1.5} - synonym:
["notebook", "computer", "laptop"] - filter:
{"field": "status", "value": "featured"} - redirect:
"/sale-page"
Match Types:
| Type | Description | Example |
|---|---|---|
| Exact | Query must match exactly | laptop matches only "laptop" |
| Contains | Query must contain pattern | laptop matches "best laptop deals" |
| Prefix | Query must start with pattern | lap matches "laptop", "lapel" |
| Regex | Regular expression pattern | ^(buy|purchase) matches "buy..." or "purchase..." |
Multi-Language Patterns:
Use commas to match multiple patterns in one rule (Exact, Contains, Prefix):
sale, تخفيض, soldes, angebot
This matches "sale" (EN), "تخفيض" (AR), "soldes" (FR), or "angebot" (DE).
For Regex, use the | operator instead:
^(sale|تخفيض|soldes|angebot)
Multi-Language Support
Search Manager automatically handles multiple languages:
// config/search-manager.php 'indices' => [ 'entries-en' => [ 'siteId' => 1, 'language' => 'en', // Optional override (auto-detected from site) ], 'entries-ar' => [ 'siteId' => 2, 'language' => 'ar', // Arabic with regional fallback ], 'all-entries' => [ 'siteId' => null, // All sites - language per document ], ],
Supported Languages:
- English (en) - 297 stop words + AND/OR/NOT operators
- Arabic (ar) - 122 stop words + و/أو/او/ليس/لا operators (supports spelling variations)
- German (de) - 130+ stop words + UND/ODER/NICHT operators
- French (fr) - 140+ stop words + ET/OU/SAUF operators
- Spanish (es) - 135+ stop words + Y/O/NO operators
Localized Boolean Operators:
Each language supports native boolean operators (case-insensitive):
| Language | AND | OR | NOT | Example |
|---|---|---|---|---|
| English | AND | OR | NOT | coffee OR tea NOT decaf |
| German | UND | ODER | NICHT | kaffee ODER tee NICHT entkoffeiniert |
| French | ET | OU | SAUF | café OU thé SAUF décaféiné |
| Spanish | Y | O | NO | café O té NO descafeinado |
| Arabic | و | أو / او | ليس / لا | قهوة او شاي لا منزوع |
Note: English operators always work as fallback regardless of site language.
Regional Variants:
# Create regional stop words mkdir -p config/search-manager/stopwords cp vendor/.../src/search/stopwords/ar.php config/search-manager/stopwords/ar-sa.php # Edit ar-sa.php for Saudi-specific terms
Language Filtering:
{# Search specific language #} {% set enResults = craft.searchManager.search('all-entries', 'test', { language: 'en' // Only English results }) %} {# Auto-detects from current site #} {% set results = craft.searchManager.search('all-entries', 'test') %} {# On English site → filters to 'en' automatically #}
Fallback Chain:
ar-sa → config/ar-sa.php → plugin/ar-sa.php → config/ar.php → plugin/ar.php
Auto-Indexing
Elements are automatically indexed when saved if autoIndex is enabled in settings.
Manual Indexing
use lindemannrock\searchmanager\SearchManager; // Index single element SearchManager::$plugin->indexing->indexElement($entry); // Rebuild an index SearchManager::$plugin->indexing->rebuildIndex('entries-en'); // Rebuild all indices SearchManager::$plugin->indexing->rebuildAll();
Backend Configuration
Algolia
'backends' => [ 'algolia' => [ 'enabled' => true, 'applicationId' => App::env('ALGOLIA_APPLICATION_ID'), 'adminApiKey' => App::env('ALGOLIA_ADMIN_API_KEY'), 'searchApiKey' => App::env('ALGOLIA_SEARCH_API_KEY'), 'timeout' => 5, 'connectTimeout' => 1, ], ],
File (Built-in)
'backends' => [ 'file' => [ 'enabled' => true, // Index data: storage/runtime/search-manager/indices/ // Search cache: storage/runtime/search-manager/cache/search/ // Device cache: storage/runtime/search-manager/cache/device/ ], ],
Meilisearch
'backends' => [ 'meilisearch' => [ 'enabled' => true, 'host' => 'http://localhost:7700', 'apiKey' => App::env('MEILISEARCH_API_KEY'), 'timeout' => 5, ], ],
MySQL / PostgreSQL (Built-in)
Uses Craft's existing database connection - no additional configuration needed.
'backends' => [ 'mysql' => [ 'enabled' => true, // Uses Craft's MySQL database // No additional config needed ], // Or for PostgreSQL installations: 'pgsql' => [ 'enabled' => true, // Uses Craft's PostgreSQL database // No additional config needed ], ],
Note: Only the backend matching your Craft database will be available. MySQL backend requires Craft to use MySQL, PostgreSQL backend requires Craft to use PostgreSQL.
Redis
Option 1: Reuse Craft's Redis Cache (No Config Needed)
If Craft is configured to use Redis cache in config/app.php, Search Manager can automatically reuse that connection:
'backends' => [ 'redis' => [ 'enabled' => true, // Leave all fields empty in CP or omit config entirely // Plugin will automatically use Craft's Redis settings ], ],
Option 2: Dedicated Redis Connection
Configure a separate Redis connection for search:
'backends' => [ 'redis' => [ 'enabled' => true, 'host' => App::env('REDIS_HOST') ?: 'redis', 'port' => App::env('REDIS_PORT') ?: 6379, 'password' => App::env('REDIS_PASSWORD'), 'database' => App::env('REDIS_DATABASE') ?: 0, ], ],
Environment Variables (.env):
REDIS_HOST=redis REDIS_PORT=6379 REDIS_PASSWORD= REDIS_DATABASE=0
Or via Control Panel:
- Leave all fields empty to reuse Craft's Redis cache
- Or use
$REDIS_HOSTformat in settings (plugin resolves environment variables automatically) - Required fields: Host, Port, Database (Password is optional)
Typesense
'backends' => [ 'typesense' => [ 'enabled' => true, 'host' => 'localhost', 'port' => '8108', 'protocol' => 'http', 'apiKey' => App::env('TYPESENSE_API_KEY'), 'connectionTimeout' => 5, ], ],
Utilities & Cache Management
Plugin Utilities (Control Panel → Utilities → Search Manager)
Index Management
- Rebuild All Indices - Refresh all indexed data from Craft elements
- Clear Backend Storage - Delete all indexed data (File/MySQL/Redis/Algolia/etc.)
- Adapts to your active backend automatically
- Shows current storage count (files, rows, or indices)
Cache Management
- Clear Device Cache - Delete cached device detection results
- Clear Search Cache - Delete cached search query results
- Clear All Caches - Clear both device and search caches
- Only shows when at least one cache is enabled
- Shows cache file counts in real-time
Analytics Data Management
- Clear All Analytics - Permanently delete all search tracking data
- Double confirmation required (destructive action)
- Only shows when analytics is enabled
Craft's Clear Caches Utility
Search Manager integrates with Craft's built-in Clear Caches utility:
- {pluginName} search caches - Clear cached search results (safe, auto-regenerate on next search)
Note: Index clearing is intentionally not available in Clear Caches because clearing indices breaks search until manually rebuilt. Use the plugin's "Rebuild All Indices" utility action instead.
Console Commands
Index Management
# List all indices php craft search-manager/index/list # Rebuild all indices php craft search-manager/index/rebuild # Rebuild specific index php craft search-manager/index/rebuild entries-en # Clear all indices php craft search-manager/index/clear # Clear specific index php craft search-manager/index/clear entries-en
Security & Analytics
# Generate IP hash salt for analytics (REQUIRED for analytics) php craft search-manager/security/generate-salt # With DDEV ddev craft search-manager/security/generate-salt
Important: Run generate-salt immediately after installation to enable analytics tracking.
Events
use lindemannrock\searchmanager\services\IndexingService; use lindemannrock\searchmanager\events\IndexEvent; use yii\base\Event; // Modify data before indexing Event::on( IndexingService::class, IndexingService::EVENT_BEFORE_INDEX, function(IndexEvent $event) { // Modify $event->element or set $event->isValid = false to cancel } ); // React after indexing Event::on( IndexingService::class, IndexingService::EVENT_AFTER_INDEX, function(IndexEvent $event) { // Access $event->data (indexed document) // Access $event->indexHandle } );
Permissions
- View indices: Can view search indices in CP
- Manage indices: Can create, edit, delete indices
- Create indices: Can create new indices
- Edit indices: Can edit existing indices
- Delete indices: Can delete indices
- Rebuild indices: Can rebuild indices
- Manage promotions: Can create, edit, delete promotions (pinned results)
- Manage query rules: Can create, edit, delete query rules (synonyms, boosts, etc.)
- View analytics: Can view analytics dashboard and search statistics
- Export analytics: Can export analytics data
- View logs: Can view plugin logs
- Manage settings: Can change plugin settings
Configuration
General Settings
return [ '*' => [ // Plugin display name 'pluginName' => 'Search Manager', // Logging level: debug, info, warning, error 'logLevel' => 'error', // Auto-index elements when saved 'autoIndex' => true, // Use queue for indexing operations 'queueEnabled' => true, // Replace Craft's native search service 'replaceNativeSearch' => false, // Batch size for bulk operations // Reduce if experiencing memory issues with large relational data 'batchSize' => 100, // Prefix for index names (useful for multi-environment) 'indexPrefix' => App::env('SEARCH_INDEX_PREFIX'), // Active search backend (mysql, pgsql, redis, file, algolia, meilisearch, typesense) 'searchBackend' => 'mysql', // Analytics settings 'enableAnalytics' => true, 'analyticsRetention' => 90, // days 'anonymizeIpAddress' => false, // Subnet masking for privacy 'enableGeoDetection' => false, // Track visitor location 'ipHashSalt' => App::env('SEARCH_MANAGER_IP_SALT'), 'defaultCountry' => App::env('SEARCH_MANAGER_DEFAULT_COUNTRY') ?: 'AE', 'defaultCity' => App::env('SEARCH_MANAGER_DEFAULT_CITY') ?: 'Dubai', // BM25 Algorithm Parameters (MySQL, Redis, File backends) 'bm25K1' => 1.5, 'bm25B' => 0.75, 'titleBoostFactor' => 5.0, 'exactMatchBoostFactor' => 3.0, 'ngramSizes' => '2,3', 'similarityThreshold' => 0.50, 'maxFuzzyCandidates' => 100, // Cache settings 'enableCache' => true, // Enable search results caching 'cacheStorageMethod' => 'file', // Storage: 'file' or 'redis' 'cacheDuration' => 3600, // Cache TTL in seconds (3600 = 1 hour) 'cachePopularQueriesOnly' => false, // Only cache frequently-searched queries 'popularQueryThreshold' => 5, // Minimum search count before caching 'cacheDeviceDetection' => true, // Cache device detection results 'deviceDetectionCacheDuration' => 3600, // Device cache TTL in seconds ], ];
Environment-Specific Configuration
return [ '*' => [ 'searchBackend' => 'mysql', 'logLevel' => 'error', 'enableAnalytics' => true, ], 'dev' => [ 'logLevel' => 'debug', 'indexPrefix' => 'dev_', 'queueEnabled' => false, 'enableCache' => false, // Disable cache for testing 'cacheDuration' => 300, // 5 minutes (if enabled) 'deviceDetectionCacheDuration' => 1800, // 30 minutes 'analyticsRetention' => 30, 'backends' => [ 'mysql' => ['enabled' => true], 'file' => ['enabled' => true], ], ], 'staging' => [ 'logLevel' => 'info', 'indexPrefix' => 'staging_', 'enableCache' => true, 'cacheStorageMethod' => 'redis', // Use Redis for edge networks 'cacheDuration' => 1800, // 30 minutes 'deviceDetectionCacheDuration' => 3600, // 1 hour 'analyticsRetention' => 90, 'backends' => [ 'redis' => ['enabled' => true], ], ], 'production' => [ 'logLevel' => 'error', 'indexPrefix' => 'prod_', 'queueEnabled' => true, 'enableCache' => true, 'cacheStorageMethod' => 'redis', // Use Redis for edge networks (Servd/AWS/Platform.sh) 'cacheDuration' => 7200, // 2 hours (optimize for performance) 'deviceDetectionCacheDuration' => 86400, // 24 hours 'cachePopularQueriesOnly' => true, // Save cache space 'popularQueryThreshold' => 3, // Cache after 3 searches 'analyticsRetention' => 365, 'enableGeoDetection' => true, 'backends' => [ 'algolia' => ['enabled' => true], ], ], ];
Performance & Troubleshooting
Memory Issues During Indexing
⚠️ If you experience memory exhaustion errors during index rebuilds:
Symptoms:
PHP Fatal error: Allowed memory size of 536870912 bytes exhausted
Queue job failed with memory error
Common Causes:
- AutoTransformer loading large amounts of relational data (Entries, Categories, Matrix blocks)
- Products with many related entries (20+ relations per product)
- Batch size too large for available memory
- Deeply nested Matrix fields
Solutions:
1. Reduce Batch Size (Recommended)
// config/search-manager.php return [ '*' => [ 'batchSize' => 100, // Default ], 'staging' => [ 'batchSize' => 10, // Smaller for memory-constrained environments ], 'production' => [ 'batchSize' => 25, // Balance between speed and memory ], ];
Guidelines:
- ✅ Default (100): Works for simple entries without many relations
- ✅ Medium (25-50): Good for entries with moderate relational fields
- ✅ Small (10-25): Use when entries have extensive relational data
- ✅ Very Small (5-10): Last resort for extremely complex data structures
2. Increase PHP Memory Limit
The rebuild job automatically increases memory to 1GB, but you may need more:
// In your .env or php.ini memory_limit = 2G
3. Optimize AutoTransformer
If you don't need to index all relational field data, create a custom transformer:
class ProductTransformer extends BaseTransformer { public function transform(ElementInterface $element): array { $data = $this->getCommonData($element); // Only index specific fields (avoid loading all relations) $data['content'] = $element->description; $data['sku'] = $element->sku; // Don't traverse deep relational fields // $data['related'] = ... // Skip if causing memory issues return $data; } }
Memory Usage Reference:
- 100 simple entries: ~50-100MB
- 100 entries with 5-10 relations each: ~200-400MB
- 100 entries with 20+ relations each: ~500MB-1GB
- 341 products with extensive relations: ~500MB+ (reduce batch size to 10-25)
Best Practices:
- Monitor memory usage in production logs
- Start with default batch size (100)
- Reduce if seeing memory errors
- Use staging environment to test optimal batch size
- Consider custom transformers for complex data
Fuzzy Search Tuning
Fuzzy matching uses n-gram similarity for typo tolerance. Default threshold is 0.50 (balanced).
If you see too many false positives:
Symptoms:
- "freezab" finds "free" (too different)
- "suga" finds unrelated terms
- Search results include irrelevant matches
Solution - Increase Similarity Threshold:
// config/search-manager.php return [ '*' => [ 'similarityThreshold' => 0.60, // Stricter matching ], ];
If you want more lenient matching:
Symptoms:
- Common typos not found ("teh" doesn't find "the")
- Missing relevant results
- Need more typo tolerance
Solution - Decrease Similarity Threshold:
// config/search-manager.php return [ '*' => [ 'similarityThreshold' => 0.35, // More lenient ], ];
Threshold Guidelines:
- ✅ 0.25: Maximum typo tolerance, more false positives
- ✅ 0.35: Good typo tolerance, some false positives
- ✅ 0.50 (Default): Balanced - good typos, fewer false positives
- ✅ 0.60: Strict - only very similar terms
- ✅ 0.75: Very strict - almost exact matches only
Test and adjust based on your content and search behavior.
Migrating from Scout
1. Install Search Manager
composer require lindemannrock/craft-search-manager php craft plugin/install search-manager
2. Copy Indices Configuration
Convert your Scout indices from config/scout.php to config/search-manager.php:
Scout format:
'indices' => [ \rias\scout\ScoutIndex::create('entries-en') ->elementType(\craft\elements\Entry::class) ->criteria(fn($query) => $query->section('news')->siteId(1)) ->transformer(new \modules\transformers\EntryTransformer()), ],
Search Manager format:
'indices' => [ 'entries-en' => [ 'name' => 'Entries (English)', 'elementType' => \craft\elements\Entry::class, 'siteId' => 1, 'criteria' => fn($query) => $query->section('news'), 'transformer' => \modules\transformers\EntryTransformer::class, 'enabled' => true, ], ],
3. Update Transformers
Change transformer parent class:
Scout:
use League\Fractal\TransformerAbstract; class EntryTransformer extends TransformerAbstract { public function transform(Entry $entry) { ... } }
Search Manager:
use lindemannrock\searchmanager\transformers\BaseTransformer; class EntryTransformer extends BaseTransformer { protected function getElementType(): string { return Entry::class; } public function transform(ElementInterface $element): array { ... } }
4. Rebuild Indices
php craft search-manager/index/rebuild
5. Remove Scout
composer remove rias/craft-scout
Logging
Search Manager uses the LindemannRock Logging Library for centralized logging.
Log Levels
- Error: Critical errors only (default)
- Warning: Errors and warnings
- Info: General information
- Debug: Detailed debugging (requires devMode)
Configuration
// config/search-manager.php return [ 'logLevel' => 'error', // error, warning, info, or debug ];
Note: Debug level requires Craft's devMode to be enabled. If set to debug with devMode disabled, it automatically falls back to info level.
Log Files
- Location:
storage/logs/search-manager-YYYY-MM-DD.log - Retention: 30 days (automatic cleanup via Logging Library)
- Format: Structured JSON logs with context data
- Web Interface: View and filter logs in CP at Search Manager → Logs
Log Management
Access logs through the Control Panel:
- Navigate to Search Manager → Logs
- Filter by date, level, or search terms
- Download log files for external analysis
- View file sizes and entry counts
- Auto-cleanup after 30 days (configurable via Logging Library)
Requires: lindemannrock/craft-logging-library plugin (installed automatically as dependency)
Support
- Documentation: https://github.com/LindemannRock/craft-search-manager
- Issues: https://github.com/LindemannRock/craft-search-manager/issues
- Email: support@lindemannrock.com
License
This plugin is licensed under the MIT License. See LICENSE for details.
Credits
Created by LindemannRock