$host['host'], 'port' => $host['port'] ?? 9200, ]; if (! empty($host['scheme'])) { $hostConfig['scheme'] = $host['scheme']; } if (! empty($host['user']) && ! empty($host['pass'])) { $hostConfig['user'] = $host['user']; $hostConfig['pass'] = $host['pass']; } $hosts[] = $hostConfig; } $clientBuilder->setHosts($hosts); // Explicitly set cURL handler $clientBuilder->setHandler(new CurlHandler); // Set connection timeout and other options $clientBuilder->setConnectionParams([ 'timeout' => 10, 'connect_timeout' => 5, ]); self::$client = $clientBuilder->build(); } catch (\Throwable $e) { Log::error('Failed to initialize Elasticsearch client: '.$e->getMessage()); throw new \RuntimeException('Elasticsearch client initialization failed: '.$e->getMessage()); } } return self::$client; } /** * Check if Elasticsearch is available */ private function isElasticsearchAvailable(): bool { try { $client = $this->getClient(); $client->ping(); return true; } catch (\Throwable $e) { Log::warning('Elasticsearch is not available: '.$e->getMessage()); return false; } } public function indexSearch(array|string $phrases, int $limit): array|Collection { if (empty($phrases) || ! $this->isElasticsearchAvailable()) { return []; } $keywords = $this->sanitizeSearchTerms($phrases); if (empty($keywords)) { return []; } // Create cache key $cacheKey = md5('es_index_search_'.serialize([$keywords, $limit])); $cached = Cache::get($cacheKey); if ($cached !== null) { return $cached; } try { $search = [ 'scroll' => self::SCROLL_TIMEOUT, 'index' => 'releases', 'body' => [ 'query' => [ 'query_string' => [ 'query' => $keywords, 'fields' => ['searchname^2', 'plainsearchname^1.5', 'fromname', 'filename', 'name^1.2', 'categories_id'], 'analyze_wildcard' => true, 'default_operator' => 'and', 'fuzziness' => 'AUTO', 'minimum_should_match' => '75%', ], ], 'size' => min($limit, self::MAX_RESULTS), 'sort' => [ ['_score' => ['order' => 'desc']], ['add_date' => ['order' => 'desc']], ['post_date' => ['order' => 'desc']], ], '_source' => ['id'], ], ]; $result = $this->search($search); Cache::put($cacheKey, $result, now()->addMinutes(self::CACHE_TTL_MINUTES)); return $result; } catch (ElasticsearchException $e) { Log::error('ElasticSearch indexSearch error: '.$e->getMessage(), [ 'keywords' => $keywords, 'limit' => $limit, ]); return []; } } public function indexSearchApi(array|string $searchName, int $limit): array|Collection { if (empty($searchName) || ! $this->isElasticsearchAvailable()) { return []; } $keywords = $this->sanitizeSearchTerms($searchName); if (empty($keywords)) { return []; } $cacheKey = md5('es_api_search_'.serialize([$keywords, $limit])); $cached = Cache::get($cacheKey); if ($cached !== null) { return $cached; } try { $search = [ 'scroll' => self::SCROLL_TIMEOUT, 'index' => 'releases', 'body' => [ 'query' => [ 'query_string' => [ 'query' => $keywords, 'fields' => ['searchname^2', 'plainsearchname^1.5', 'fromname', 'filename', 'name^1.2'], 'analyze_wildcard' => true, 'default_operator' => 'and', 'fuzziness' => 'AUTO', ], ], 'size' => min($limit, self::MAX_RESULTS), 'sort' => [ ['_score' => ['order' => 'desc']], ['add_date' => ['order' => 'desc']], ['post_date' => ['order' => 'desc']], ], '_source' => ['id'], ], ]; $result = $this->search($search); Cache::put($cacheKey, $result, now()->addMinutes(self::CACHE_TTL_MINUTES)); return $result; } catch (ElasticsearchException $e) { Log::error('ElasticSearch indexSearchApi error: '.$e->getMessage(), [ 'keywords' => $keywords, 'limit' => $limit, ]); return []; } } public function indexSearchTMA(array|string $name, int $limit): array|Collection { if (empty($name) || ! $this->isElasticsearchAvailable()) { return []; } $keywords = $this->sanitizeSearchTerms($name); if (empty($keywords)) { return []; } $cacheKey = md5('es_tma_search_'.serialize([$keywords, $limit])); $cached = Cache::get($cacheKey); if ($cached !== null) { return $cached; } try { $search = [ 'scroll' => self::SCROLL_TIMEOUT, 'index' => 'releases', 'body' => [ 'query' => [ 'query_string' => [ 'query' => $keywords, 'fields' => ['searchname^2', 'plainsearchname^1.5'], 'analyze_wildcard' => true, 'default_operator' => 'and', 'boost' => 1.2, ], ], 'size' => min($limit, self::MAX_RESULTS), 'sort' => [ ['_score' => ['order' => 'desc']], ['add_date' => ['order' => 'desc']], ['post_date' => ['order' => 'desc']], ], '_source' => ['id'], ], ]; $result = $this->search($search); Cache::put($cacheKey, $result, now()->addMinutes(self::CACHE_TTL_MINUTES)); return $result; } catch (ElasticsearchException $e) { Log::error('ElasticSearch indexSearchTMA error: '.$e->getMessage(), [ 'keywords' => $keywords, 'limit' => $limit, ]); return []; } } public function predbIndexSearch(array|string $searchTerm): array|Collection { if (empty($searchTerm) || ! $this->isElasticsearchAvailable()) { return []; } $keywords = $this->sanitizeSearchTerms($searchTerm); if (empty($keywords)) { return []; } $cacheKey = md5('es_predb_search_'.$keywords); $cached = Cache::get($cacheKey); if ($cached !== null) { return $cached; } try { $search = [ 'scroll' => self::SCROLL_TIMEOUT, 'index' => 'predb', 'body' => [ 'query' => [ 'query_string' => [ 'query' => $keywords, 'fields' => ['title^2', 'filename'], 'analyze_wildcard' => true, 'default_operator' => 'and', 'fuzziness' => 'AUTO', ], ], 'size' => 1000, 'sort' => [ ['_score' => ['order' => 'desc']], ], ], ]; $result = $this->search($search, true); Cache::put($cacheKey, $result, now()->addMinutes(self::CACHE_TTL_MINUTES)); return $result; } catch (ElasticsearchException $e) { Log::error('ElasticSearch predbIndexSearch error: '.$e->getMessage(), [ 'keywords' => $keywords, ]); return []; } } public function insertRelease(array $parameters): void { if (empty($parameters['id']) || ! $this->isElasticsearchAvailable()) { if (empty($parameters['id'])) { Log::warning('ElasticSearch: Cannot insert release without ID'); } return; } try { $searchNameDotless = str_replace(['.', '-'], ' ', $parameters['searchname'] ?? ''); $data = [ 'body' => [ 'id' => $parameters['id'], 'name' => $parameters['name'] ?? '', 'searchname' => $parameters['searchname'] ?? '', 'plainsearchname' => $searchNameDotless, 'fromname' => $parameters['fromname'] ?? '', 'categories_id' => $parameters['categories_id'] ?? 0, 'filename' => $parameters['filename'] ?? '', 'add_date' => now()->format('Y-m-d H:i:s'), 'post_date' => $parameters['postdate'] ?? now()->format('Y-m-d H:i:s'), ], 'index' => 'releases', 'id' => $parameters['id'], ]; $client = $this->getClient(); $client->index($data); } catch (ElasticsearchException $e) { Log::error('ElasticSearch insertRelease error: '.$e->getMessage(), [ 'release_id' => $parameters['id'], ]); } catch (\Throwable $e) { Log::error('ElasticSearch insertRelease unexpected error: '.$e->getMessage(), [ 'release_id' => $parameters['id'], ]); } } public function updateRelease(int $id): void { if (empty($id)) { Log::warning('ElasticSearch: Cannot update release without ID'); return; } try { $release = Release::query() ->where('releases.id', $id) ->leftJoin('release_files as rf', 'releases.id', '=', 'rf.releases_id') ->select([ 'releases.id', 'releases.name', 'releases.searchname', 'releases.fromname', 'releases.categories_id', DB::raw('IFNULL(GROUP_CONCAT(rf.name SEPARATOR " "),"") filename'), ]) ->groupBy('releases.id') ->first(); if ($release === null) { Log::warning('ElasticSearch: Release not found for update', ['id' => $id]); return; } $searchNameDotless = str_replace(['.', '-'], ' ', $release->searchname); $data = [ 'body' => [ 'doc' => [ 'id' => $release->id, 'name' => $release->name, 'searchname' => $release->searchname, 'plainsearchname' => $searchNameDotless, 'fromname' => $release->fromname, 'categories_id' => $release->categories_id, 'filename' => $release->filename, ], 'doc_as_upsert' => true, ], 'index' => 'releases', 'id' => $release->id, ]; Elasticsearch::update($data); } catch (ElasticsearchException $e) { Log::error('ElasticSearch updateRelease error: '.$e->getMessage(), [ 'release_id' => $id, ]); } catch (\Throwable $e) { Log::error('ElasticSearch updateRelease unexpected error: '.$e->getMessage(), [ 'release_id' => $id, ]); } } public function searchPreDb($searchTerm): array { if (empty($searchTerm)) { return []; } $keywords = $this->sanitizeSearchTerms($searchTerm); if (empty($keywords)) { return []; } $search = [ 'index' => 'predb', 'body' => [ 'query' => [ 'query_string' => [ 'query' => $keywords, 'fields' => ['title^2', 'filename'], 'analyze_wildcard' => true, 'default_operator' => 'and', 'fuzziness' => 'AUTO', ], ], 'size' => 100, 'sort' => [ ['_score' => ['order' => 'desc']], ], ], ]; try { $primaryResults = Elasticsearch::search($search); return $primaryResults['hits']['hits'] ?? []; } catch (ElasticsearchException $e) { Log::error('ElasticSearch searchPreDb error: '.$e->getMessage(), [ 'search_term' => $searchTerm, ]); return []; } } public function insertPreDb(array $parameters): void { if (empty($parameters['id'])) { Log::warning('ElasticSearch: Cannot insert predb without ID'); return; } try { $data = [ 'body' => [ 'id' => $parameters['id'], 'title' => $parameters['title'] ?? '', 'source' => $parameters['source'] ?? '', 'filename' => $parameters['filename'] ?? '', ], 'index' => 'predb', 'id' => $parameters['id'], ]; Elasticsearch::index($data); } catch (ElasticsearchException $e) { Log::error('ElasticSearch insertPreDb error: '.$e->getMessage(), [ 'predb_id' => $parameters['id'], ]); } catch (\Throwable $e) { Log::error('ElasticSearch insertPreDb unexpected error: '.$e->getMessage(), [ 'predb_id' => $parameters['id'], ]); } } public function updatePreDb(array $parameters): void { if (empty($parameters['id'])) { Log::warning('ElasticSearch: Cannot update predb without ID'); return; } try { $data = [ 'body' => [ 'doc' => [ 'id' => $parameters['id'], 'title' => $parameters['title'] ?? '', 'filename' => $parameters['filename'] ?? '', 'source' => $parameters['source'] ?? '', ], 'doc_as_upsert' => true, ], 'index' => 'predb', 'id' => $parameters['id'], ]; Elasticsearch::update($data); } catch (ElasticsearchException $e) { Log::error('ElasticSearch updatePreDb error: '.$e->getMessage(), [ 'predb_id' => $parameters['id'], ]); } catch (\Throwable $e) { Log::error('ElasticSearch updatePreDb unexpected error: '.$e->getMessage(), [ 'predb_id' => $parameters['id'], ]); } } /** * Sanitize search terms for Elasticsearch */ private function sanitizeSearchTerms(array|string $terms): string { if (is_array($terms)) { $terms = implode(' ', array_filter($terms)); } if (function_exists('sanitize')) { return sanitize($terms); } // Fallback sanitization if sanitize function doesn't exist $terms = trim($terms); if (empty($terms)) { return ''; } // Remove dangerous characters but keep wildcards $terms = preg_replace('/[<>"\'\x00-\x1f\x7f]/', '', $terms); // Escape special Elasticsearch characters except wildcards $terms = preg_replace('/([+\-={}[\]^~():"\\\\])/', '\\\\$1', $terms); return $terms; } protected function search(array $search, bool $fullResults = false): array { if (empty($search) || ! $this->isElasticsearchAvailable()) { return []; } try { $client = $this->getClient(); $results = $client->search($search); $searchResult = []; while (isset($results['hits']['hits']) && count($results['hits']['hits']) > 0) { foreach ($results['hits']['hits'] as $result) { if ($fullResults === true) { $searchResult[] = $result['_source']; } else { $searchResult[] = $result['_source']['id'] ?? $result['_id']; } } // Handle scrolling for large result sets if (! isset($results['_scroll_id'])) { break; } $scroll_id = $results['_scroll_id']; $results = $client->scroll([ 'scroll_id' => $scroll_id, 'scroll' => self::SCROLL_TIMEOUT, ]); } return $searchResult; } catch (ElasticsearchException $e) { Log::error('ElasticSearch search error: '.$e->getMessage()); return []; } catch (\Throwable $e) { Log::error('ElasticSearch search unexpected error: '.$e->getMessage()); return []; } } public function deleteRelease(int $id): void { if (empty($id) || ! $this->isElasticsearchAvailable()) { if (empty($id)) { Log::warning('ElasticSearch: Cannot delete release without ID'); } return; } try { $data = [ 'index' => 'releases', 'id' => $id, ]; $client = $this->getClient(); $client->delete($data); } catch (ElasticsearchException $e) { Log::error('ElasticSearch deleteRelease error: '.$e->getMessage(), [ 'release_id' => $id, ]); } catch (\Throwable $e) { Log::error('ElasticSearch deleteRelease unexpected error: '.$e->getMessage(), [ 'release_id' => $id, ]); } } }