Skip to content

Commit 6dc4c6e

Browse files
Merge pull request #28 from ginkelsoft-development/feature/search-result-caching
add SearchCacheService for caching encrypted search results
2 parents e53a811 + b53c594 commit 6dc4c6e

File tree

1 file changed

+223
-0
lines changed

1 file changed

+223
-0
lines changed
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
<?php
2+
3+
namespace Ginkelsoft\EncryptedSearch\Services;
4+
5+
use Illuminate\Support\Facades\Cache;
6+
7+
/**
8+
* Class SearchCacheService
9+
*
10+
* Provides caching functionality for encrypted search results.
11+
*
12+
* Caches search results to reduce database/Elasticsearch queries for
13+
* frequently performed searches. Automatically invalidates cache when
14+
* models are updated or deleted.
15+
*
16+
* Cache keys are generated based on:
17+
* - Model class
18+
* - Search type (exact/prefix/any/all)
19+
* - Search parameters (fields, terms)
20+
* - Configuration hash (pepper, depths, etc.)
21+
*
22+
* Example usage:
23+
* ```php
24+
* $service = app(SearchCacheService::class);
25+
* $results = $service->remember('Client', 'exact', ['first_names', 'John'], function() {
26+
* return Client::encryptedExact('first_names', 'John')->pluck('id')->toArray();
27+
* });
28+
* ```
29+
*/
30+
class SearchCacheService
31+
{
32+
/**
33+
* Cache TTL in seconds (default: 1 hour).
34+
*
35+
* @var int
36+
*/
37+
protected int $ttl;
38+
39+
/**
40+
* Cache key prefix to avoid collisions.
41+
*
42+
* @var string
43+
*/
44+
protected string $prefix = 'encrypted_search:';
45+
46+
/**
47+
* Create a new SearchCacheService instance.
48+
*/
49+
public function __construct()
50+
{
51+
$this->ttl = (int) config('encrypted-search.cache.ttl', 3600);
52+
}
53+
54+
/**
55+
* Remember a search result in cache.
56+
*
57+
* @param string $modelClass The model class name
58+
* @param string $searchType The search type (exact/prefix/any/all)
59+
* @param array<mixed> $params Search parameters
60+
* @param callable $callback Callback to execute if cache miss
61+
* @return array<int, mixed> Array of model IDs
62+
*/
63+
public function remember(string $modelClass, string $searchType, array $params, callable $callback): array
64+
{
65+
if (!$this->isEnabled()) {
66+
return $callback();
67+
}
68+
69+
$key = $this->generateKey($modelClass, $searchType, $params);
70+
71+
return Cache::remember($key, $this->ttl, $callback);
72+
}
73+
74+
/**
75+
* Invalidate all cached searches for a specific model instance.
76+
*
77+
* Called when a model is updated or deleted.
78+
*
79+
* @param string $modelClass The model class name
80+
* @param mixed $modelId The model ID (optional, if null invalidates all for class)
81+
* @return void
82+
*/
83+
public function invalidate(string $modelClass, $modelId = null): void
84+
{
85+
if (!$this->isEnabled()) {
86+
return;
87+
}
88+
89+
// Invalidate by tag if driver supports it (Redis, Memcached)
90+
if ($this->supportsTagging()) {
91+
$tags = $modelId
92+
? [$this->getModelTag($modelClass), $this->getInstanceTag($modelClass, $modelId)]
93+
: [$this->getModelTag($modelClass)];
94+
95+
Cache::tags($tags)->flush();
96+
} else {
97+
// Fallback: invalidate by pattern (only works with some drivers)
98+
$this->invalidateByPattern($modelClass, $modelId);
99+
}
100+
}
101+
102+
/**
103+
* Completely flush all encrypted search caches.
104+
*
105+
* @return void
106+
*/
107+
public function flush(): void
108+
{
109+
if (!$this->isEnabled()) {
110+
return;
111+
}
112+
113+
if ($this->supportsTagging()) {
114+
Cache::tags(['encrypted_search'])->flush();
115+
} else {
116+
// Note: This is cache driver specific and may not work everywhere
117+
Cache::flush();
118+
}
119+
}
120+
121+
/**
122+
* Generate a unique cache key for a search.
123+
*
124+
* @param string $modelClass
125+
* @param string $searchType
126+
* @param array<mixed> $params
127+
* @return string
128+
*/
129+
protected function generateKey(string $modelClass, string $searchType, array $params): string
130+
{
131+
// Include configuration hash to auto-invalidate when config changes
132+
$configHash = $this->getConfigHash();
133+
134+
// Create deterministic key from parameters
135+
$paramsHash = md5(json_encode($params));
136+
137+
return $this->prefix . md5(
138+
$modelClass . ':' . $searchType . ':' . $paramsHash . ':' . $configHash
139+
);
140+
}
141+
142+
/**
143+
* Get a hash of relevant configuration values.
144+
*
145+
* When configuration changes, all caches should be invalidated.
146+
*
147+
* @return string
148+
*/
149+
protected function getConfigHash(): string
150+
{
151+
return md5(json_encode([
152+
config('encrypted-search.search_pepper'),
153+
config('encrypted-search.max_prefix_depth'),
154+
config('encrypted-search.min_prefix_length'),
155+
]));
156+
}
157+
158+
/**
159+
* Get cache tag for a model class.
160+
*
161+
* @param string $modelClass
162+
* @return string
163+
*/
164+
protected function getModelTag(string $modelClass): string
165+
{
166+
return 'encrypted_search:model:' . md5($modelClass);
167+
}
168+
169+
/**
170+
* Get cache tag for a specific model instance.
171+
*
172+
* @param string $modelClass
173+
* @param mixed $modelId
174+
* @return string
175+
*/
176+
protected function getInstanceTag(string $modelClass, $modelId): string
177+
{
178+
return 'encrypted_search:instance:' . md5($modelClass . ':' . $modelId);
179+
}
180+
181+
/**
182+
* Check if the cache driver supports tagging.
183+
*
184+
* @return bool
185+
*/
186+
protected function supportsTagging(): bool
187+
{
188+
$driver = config('cache.default');
189+
$supportedDrivers = ['redis', 'memcached', 'dynamodb', 'octane'];
190+
191+
return in_array($driver, $supportedDrivers, true);
192+
}
193+
194+
/**
195+
* Invalidate cache by pattern (fallback for drivers without tagging).
196+
*
197+
* Note: This only works with some cache drivers.
198+
*
199+
* @param string $modelClass
200+
* @param mixed $modelId
201+
* @return void
202+
*/
203+
protected function invalidateByPattern(string $modelClass, $modelId = null): void
204+
{
205+
// This is a simplified implementation
206+
// In production, you might want to track cache keys in a separate store
207+
// or use a cache driver that supports pattern-based deletion
208+
209+
// For now, we'll just clear the entire cache as a safe fallback
210+
// Individual drivers can implement more efficient pattern matching
211+
Cache::flush();
212+
}
213+
214+
/**
215+
* Check if caching is enabled.
216+
*
217+
* @return bool
218+
*/
219+
protected function isEnabled(): bool
220+
{
221+
return config('encrypted-search.cache.enabled', false);
222+
}
223+
}

0 commit comments

Comments
 (0)