3 class PhabricatorSearchService
6 const KEY_REFS
= 'cluster.search.refs';
11 protected $hosts = array();
12 protected $hostsConfig;
14 protected $roles = array();
16 const STATUS_OKAY
= 'okay';
17 const STATUS_FAIL
= 'fail';
19 const ROLE_WRITE
= 'write';
20 const ROLE_READ
= 'read';
22 public function __construct(PhabricatorFulltextStorageEngine
$engine) {
23 $this->engine
= $engine;
24 $this->hostType
= $engine->getHostType();
30 public function newHost($config) {
31 $host = clone($this->hostType
);
32 $host_config = $this->config +
$config;
33 $host->setConfig($host_config);
34 $this->hosts
[] = $host;
38 public function getEngine() {
42 public function getDisplayName() {
43 return $this->hostType
->getDisplayName();
46 public function getStatusViewColumns() {
47 return $this->hostType
->getStatusViewColumns();
50 public function setConfig($config) {
51 $this->config
= $config;
53 if (!isset($config['hosts'])) {
54 $config['hosts'] = array(
56 'host' => idx($config, 'host'),
57 'port' => idx($config, 'port'),
58 'protocol' => idx($config, 'protocol'),
59 'roles' => idx($config, 'roles'),
63 foreach ($config['hosts'] as $host) {
64 $this->newHost($host);
69 public function getConfig() {
73 public static function getConnectionStatusMap() {
75 self
::STATUS_OKAY
=> array(
76 'icon' => 'fa-exchange',
78 'label' => pht('Okay'),
80 self
::STATUS_FAIL
=> array(
83 'label' => pht('Failed'),
88 public function isWritable() {
89 return (bool)$this->getAllHostsForRole(self
::ROLE_WRITE
);
92 public function isReadable() {
93 return (bool)$this->getAllHostsForRole(self
::ROLE_READ
);
96 public function getPort() {
97 return idx($this->config
, 'port');
100 public function getProtocol() {
101 return idx($this->config
, 'protocol');
105 public function getVersion() {
106 return idx($this->config
, 'version');
109 public function getHosts() {
115 * Get a random host reference with the specified role, skipping hosts which
116 * failed recent health checks.
117 * @throws PhabricatorClusterNoHostForRoleException if no healthy hosts match.
118 * @return PhabricatorSearchHost
120 public function getAnyHostForRole($role) {
121 $hosts = $this->getAllHostsForRole($role);
123 foreach ($hosts as $host) {
124 $health = $host->getHealthRecord();
125 if ($health->getIsHealthy()) {
129 throw new PhabricatorClusterNoHostForRoleException($role);
134 * Get all configured hosts for this service which have the specified role.
135 * @return PhabricatorSearchHost[]
137 public function getAllHostsForRole($role) {
138 // if the role is explicitly set to false at the top level, then all hosts
139 // have the role disabled.
140 if (idx($this->config
, $role) === false) {
145 foreach ($this->hosts
as $host) {
146 if ($host->hasRole($role)) {
154 * Get a reference to all configured fulltext search cluster services
155 * @return PhabricatorSearchService[]
157 public static function getAllServices() {
158 $cache = PhabricatorCaches
::getRequestCache();
160 $refs = $cache->getKey(self
::KEY_REFS
);
162 $refs = self
::newRefs();
163 $cache->setKey(self
::KEY_REFS
, $refs);
170 * Load all valid PhabricatorFulltextStorageEngine subclasses
172 public static function loadAllFulltextStorageEngines() {
173 return id(new PhutilClassMapQuery())
174 ->setAncestorClass('PhabricatorFulltextStorageEngine')
175 ->setUniqueMethod('getEngineIdentifier')
180 * Create instances of PhabricatorSearchService based on configuration
181 * @return PhabricatorSearchService[]
183 public static function newRefs() {
184 $services = PhabricatorEnv
::getEnvConfig('cluster.search');
185 $engines = self
::loadAllFulltextStorageEngines();
188 foreach ($services as $config) {
190 // Normally, we've validated configuration before we get this far, but
191 // make sure we don't fatal if we end up here with a bogus configuration.
192 if (!isset($engines[$config['type']])) {
195 'Configured search engine type "%s" is unknown. Valid engines '.
198 implode(', ', array_keys($engines))));
201 $engine = clone($engines[$config['type']]);
202 $cluster = new self($engine);
203 $cluster->setConfig($config);
204 $engine->setService($cluster);
213 * (re)index the document: attempt to pass the document to all writable
214 * fulltext search hosts
216 public static function reindexAbstractDocument(
217 PhabricatorSearchAbstractDocument
$document) {
219 $exceptions = array();
220 foreach (self
::getAllServices() as $service) {
221 if (!$service->isWritable()) {
225 $engine = $service->getEngine();
227 $engine->reindexAbstractDocument($document);
228 } catch (Exception
$ex) {
234 throw new PhutilAggregateException(
236 'Writes to search services failed while reindexing document "%s".',
237 $document->getPHID()),
243 * Execute a full-text query and return a list of PHIDs of matching objects.
245 * @throws PhutilAggregateException
247 public static function executeSearch(PhabricatorSavedQuery
$query) {
248 $result_set = self
::newResultSet($query);
249 return $result_set->getPHIDs();
252 public static function newResultSet(PhabricatorSavedQuery
$query) {
253 $exceptions = array();
254 // try all services until one succeeds
255 foreach (self
::getAllServices() as $service) {
256 if (!$service->isReadable()) {
261 $engine = $service->getEngine();
262 $phids = $engine->executeSearch($query);
264 return id(new PhabricatorFulltextResultSet())
266 ->setFulltextTokens($engine->getFulltextTokens());
267 } catch (PhutilSearchQueryCompilerSyntaxException
$ex) {
268 // If there's a query compilation error, return it directly to the
269 // user: they issued a query with bad syntax.
271 } catch (Exception
$ex) {
275 $msg = pht('All of the configured Fulltext Search services failed.');
276 throw new PhutilAggregateException($msg, $exceptions);