index_entity.inc 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025
  1. <?php
  2. /**
  3. * @file
  4. * Contains SearchApiIndex.
  5. */
  6. /**
  7. * Class representing a search index.
  8. */
  9. class SearchApiIndex extends Entity {
  10. // Cache values, set when the corresponding methods are called for the first
  11. // time.
  12. /**
  13. * Cached return value of datasource().
  14. *
  15. * @var SearchApiDataSourceControllerInterface
  16. */
  17. protected $datasource = NULL;
  18. /**
  19. * Cached return value of server().
  20. *
  21. * @var SearchApiServer
  22. */
  23. protected $server_object = NULL;
  24. /**
  25. * All enabled data alterations for this index.
  26. *
  27. * @var array
  28. */
  29. protected $callbacks = NULL;
  30. /**
  31. * All enabled processors for this index.
  32. *
  33. * @var array
  34. */
  35. protected $processors = NULL;
  36. /**
  37. * The properties added by data alterations on this index.
  38. *
  39. * @var array
  40. */
  41. protected $added_properties = NULL;
  42. /**
  43. * Static cache for the results of getFields().
  44. *
  45. * Can be accessed as follows: $this->fields[$only_indexed][$get_additional].
  46. *
  47. * @var array
  48. */
  49. protected $fields = array();
  50. /**
  51. * An array containing two arrays.
  52. *
  53. * At index 0, all fulltext fields of this index. At index 1, all indexed
  54. * fulltext fields of this index.
  55. *
  56. * @var array
  57. */
  58. protected $fulltext_fields = array();
  59. // Database values that will be set when object is loaded.
  60. /**
  61. * An integer identifying the index.
  62. * Immutable.
  63. *
  64. * @var integer
  65. */
  66. public $id;
  67. /**
  68. * A name to be displayed for the index.
  69. *
  70. * @var string
  71. */
  72. public $name;
  73. /**
  74. * The machine name of the index.
  75. * Immutable.
  76. *
  77. * @var string
  78. */
  79. public $machine_name;
  80. /**
  81. * A string describing the index' use to users.
  82. *
  83. * @var string
  84. */
  85. public $description;
  86. /**
  87. * The machine_name of the server with which data should be indexed.
  88. *
  89. * @var string
  90. */
  91. public $server;
  92. /**
  93. * The type of items stored in this index.
  94. * Immutable.
  95. *
  96. * @var string
  97. */
  98. public $item_type;
  99. /**
  100. * An array of options for configuring this index. The layout is as follows:
  101. * - cron_limit: The maximum number of items to be indexed per cron batch.
  102. * - index_directly: Boolean setting whether entities are indexed immediately
  103. * after they are created or updated.
  104. * - fields: An array of all indexed fields for this index. Keys are the field
  105. * identifiers, the values are arrays for specifying the field settings. The
  106. * structure of those arrays looks like this:
  107. * - type: The type set for this field. One of the types returned by
  108. * search_api_default_field_types().
  109. * - real_type: (optional) If a custom data type was selected for this
  110. * field, this type will be stored here, and "type" contain the fallback
  111. * default data type.
  112. * - boost: (optional) A boost value for terms found in this field during
  113. * searches. Usually only relevant for fulltext fields. Defaults to 1.0.
  114. * - entity_type (optional): If set, the type of this field is really an
  115. * entity. The "type" key will then just contain the primitive data type
  116. * of the ID field, meaning that servers will ignore this and merely index
  117. * the entity's ID. Components displaying this field, though, are advised
  118. * to use the entity label instead of the ID.
  119. * - additional fields: An associative array with keys and values being the
  120. * field identifiers of related entities whose fields should be displayed.
  121. * - data_alter_callbacks: An array of all data alterations available. Keys
  122. * are the alteration identifiers, the values are arrays containing the
  123. * settings for that data alteration. The inner structure looks like this:
  124. * - status: Boolean indicating whether the data alteration is enabled.
  125. * - weight: Used for sorting the data alterations.
  126. * - settings: Alteration-specific settings, configured via the alteration's
  127. * configuration form.
  128. * - processors: An array of all processors available for the index. The keys
  129. * are the processor identifiers, the values are arrays containing the
  130. * settings for that processor. The inner structure looks like this:
  131. * - status: Boolean indicating whether the processor is enabled.
  132. * - weight: Used for sorting the processors.
  133. * - settings: Processor-specific settings, configured via the processor's
  134. * configuration form.
  135. *
  136. * @var array
  137. */
  138. public $options = array();
  139. /**
  140. * A flag indicating whether this index is enabled.
  141. *
  142. * @var integer
  143. */
  144. public $enabled = 1;
  145. /**
  146. * A flag indicating whether to write to this index.
  147. *
  148. * @var integer
  149. */
  150. public $read_only = 0;
  151. /**
  152. * Constructor as a helper to the parent constructor.
  153. */
  154. public function __construct(array $values = array(), $entity_type = 'search_api_index') {
  155. parent::__construct($values, $entity_type);
  156. }
  157. /**
  158. * Execute necessary tasks for a newly created index.
  159. */
  160. public function postCreate() {
  161. try {
  162. if ($server = $this->server()) {
  163. // Tell the server about the new index.
  164. $server->addIndex($this);
  165. if ($this->enabled) {
  166. $this->queueItems();
  167. }
  168. }
  169. }
  170. catch (SearchApiException $e) {
  171. watchdog_exception('search_api', $e);
  172. }
  173. }
  174. /**
  175. * Execute necessary tasks when the index is removed from the database.
  176. */
  177. public function postDelete() {
  178. try {
  179. if ($server = $this->server()) {
  180. $server->removeIndex($this);
  181. }
  182. }
  183. catch (SearchApiException $e) {
  184. watchdog_exception('search_api', $e);
  185. }
  186. // Stop tracking entities for indexing.
  187. $this->dequeueItems();
  188. }
  189. /**
  190. * Record entities to index.
  191. */
  192. public function queueItems() {
  193. if (!$this->read_only) {
  194. try {
  195. $this->datasource()->startTracking(array($this));
  196. }
  197. catch (SearchApiException $e) {
  198. watchdog_exception('search_api', $e);
  199. }
  200. }
  201. }
  202. /**
  203. * Remove all records of entities to index.
  204. */
  205. public function dequeueItems() {
  206. try {
  207. $this->datasource()->stopTracking(array($this));
  208. }
  209. catch (SearchApiException $e) {
  210. watchdog_exception('search_api', $e);
  211. }
  212. }
  213. /**
  214. * Saves this index to the database.
  215. *
  216. * Either creates a new record or updates the existing one with the same ID.
  217. *
  218. * @return int|false
  219. * Failure to save the index will return FALSE. Otherwise, SAVED_NEW or
  220. * SAVED_UPDATED is returned depending on the operation performed. $this->id
  221. * will be set if a new index was inserted.
  222. */
  223. public function save() {
  224. if (empty($this->description)) {
  225. $this->description = NULL;
  226. }
  227. $server = FALSE;
  228. if (!empty($this->server)) {
  229. $server = search_api_server_load($this->server);
  230. if (!$server) {
  231. $vars['%server'] = $this->server;
  232. $vars['%index'] = $this->name;
  233. watchdog('search_api', 'Unknown server %server specified for index %index.', $vars, WATCHDOG_ERROR);
  234. }
  235. }
  236. if (!$server) {
  237. $this->server = NULL;
  238. $this->enabled = FALSE;
  239. }
  240. if (!empty($this->options['fields'])) {
  241. ksort($this->options['fields']);
  242. }
  243. $this->resetCaches();
  244. return parent::save();
  245. }
  246. /**
  247. * Helper method for updating entity properties.
  248. *
  249. * NOTE: You shouldn't change any properties of this object before calling
  250. * this method, as this might lead to the fields not being saved correctly.
  251. *
  252. * @param array $fields
  253. * The new field values.
  254. *
  255. * @return int|false
  256. * SAVE_UPDATED on success, FALSE on failure, 0 if the fields already had
  257. * the specified values.
  258. */
  259. public function update(array $fields) {
  260. $changeable = array('name' => 1, 'enabled' => 1, 'description' => 1, 'server' => 1, 'options' => 1, 'read_only' => 1);
  261. $changed = FALSE;
  262. foreach ($fields as $field => $value) {
  263. if (isset($changeable[$field]) && $value !== $this->$field) {
  264. $this->$field = $value;
  265. $changed = TRUE;
  266. }
  267. }
  268. // If there are no new values, just return 0.
  269. if (!$changed) {
  270. return 0;
  271. }
  272. // Reset the index's internal property cache to correctly incorporate new
  273. // settings.
  274. $this->resetCaches();
  275. return $this->save();
  276. }
  277. /**
  278. * Schedules this search index for re-indexing.
  279. *
  280. * @return bool
  281. * TRUE on success, FALSE on failure.
  282. */
  283. public function reindex() {
  284. if (!$this->server || $this->read_only) {
  285. return TRUE;
  286. }
  287. _search_api_index_reindex($this);
  288. module_invoke_all('search_api_index_reindex', $this, FALSE);
  289. return TRUE;
  290. }
  291. /**
  292. * Clears this search index and schedules all of its items for re-indexing.
  293. *
  294. * @return bool
  295. * TRUE on success, FALSE on failure.
  296. */
  297. public function clear() {
  298. if (!$this->server || $this->read_only) {
  299. return TRUE;
  300. }
  301. try {
  302. $this->server()->deleteItems('all', $this);
  303. }
  304. catch (SearchApiException $e) {
  305. watchdog_exception('search_api', $e);
  306. }
  307. _search_api_index_reindex($this);
  308. module_invoke_all('search_api_index_reindex', $this, TRUE);
  309. return TRUE;
  310. }
  311. /**
  312. * Magic method for determining which fields should be serialized.
  313. *
  314. * Don't serialize properties that are basically only caches.
  315. *
  316. * @return array
  317. * An array of properties to be serialized.
  318. */
  319. public function __sleep() {
  320. $ret = get_object_vars($this);
  321. unset($ret['server_object'], $ret['datasource'], $ret['processors'], $ret['added_properties'], $ret['fulltext_fields']);
  322. return array_keys($ret);
  323. }
  324. /**
  325. * Get the controller object of the data source used by this index.
  326. *
  327. * @throws SearchApiException
  328. * If the specified item type or data source doesn't exist or is invalid.
  329. *
  330. * @return SearchApiDataSourceControllerInterface
  331. * The data source controller for this index.
  332. */
  333. public function datasource() {
  334. if (!isset($this->datasource)) {
  335. $this->datasource = search_api_get_datasource_controller($this->item_type);
  336. }
  337. return $this->datasource;
  338. }
  339. /**
  340. * Get the entity type of items in this index.
  341. *
  342. * @return string|null
  343. * An entity type string if the items in this index are entities; NULL
  344. * otherwise.
  345. */
  346. public function getEntityType() {
  347. try {
  348. return $this->datasource()->getEntityType();
  349. }
  350. catch (SearchApiException $e) {
  351. return NULL;
  352. }
  353. }
  354. /**
  355. * Get the server this index lies on.
  356. *
  357. * @param $reset
  358. * Whether to reset the internal cache. Set to TRUE when the index' $server
  359. * property has just changed.
  360. *
  361. * @throws SearchApiException
  362. * If $this->server is set, but no server with that machine name exists.
  363. *
  364. * @return SearchApiServer
  365. * The server associated with this index, or NULL if this index currently
  366. * doesn't lie on a server.
  367. */
  368. public function server($reset = FALSE) {
  369. if (!isset($this->server_object) || $reset) {
  370. $this->server_object = $this->server ? search_api_server_load($this->server) : FALSE;
  371. if ($this->server && !$this->server_object) {
  372. throw new SearchApiException(t('Unknown server @server specified for index @name.', array('@server' => $this->server, '@name' => $this->machine_name)));
  373. }
  374. }
  375. return $this->server_object ? $this->server_object : NULL;
  376. }
  377. /**
  378. * Create a query object for this index.
  379. *
  380. * @param $options
  381. * Associative array of options configuring this query. See
  382. * SearchApiQueryInterface::__construct().
  383. *
  384. * @throws SearchApiException
  385. * If the index is currently disabled or its server doesn't exist.
  386. *
  387. * @return SearchApiQueryInterface
  388. * A query object for searching this index.
  389. */
  390. public function query($options = array()) {
  391. if (!$this->enabled) {
  392. throw new SearchApiException(t('Cannot search on a disabled index.'));
  393. }
  394. return $this->server()->query($this, $options);
  395. }
  396. /**
  397. * Indexes items on this index.
  398. *
  399. * Will return an array of IDs of items that should be marked as indexed –
  400. * i.e., items that were either rejected by a data-alter callback or were
  401. * successfully indexed.
  402. *
  403. * @param array $items
  404. * An array of items to index, of this index's item type.
  405. *
  406. * @return array
  407. * An array of the IDs of all items that should be marked as indexed.
  408. *
  409. * @throws SearchApiException
  410. * If an error occurred during indexing.
  411. */
  412. public function index(array $items) {
  413. if ($this->read_only) {
  414. return array();
  415. }
  416. if (!$this->enabled) {
  417. throw new SearchApiException(t("Couldn't index values on '@name' index (index is disabled)", array('@name' => $this->name)));
  418. }
  419. if (empty($this->options['fields'])) {
  420. throw new SearchApiException(t("Couldn't index values on '@name' index (no fields selected)", array('@name' => $this->name)));
  421. }
  422. $fields = $this->options['fields'];
  423. $custom_type_fields = array();
  424. foreach ($fields as $field => $info) {
  425. if (isset($info['real_type'])) {
  426. $custom_type = search_api_extract_inner_type($info['real_type']);
  427. if ($this->server()->supportsFeature('search_api_data_type_' . $custom_type)) {
  428. $fields[$field]['type'] = $info['real_type'];
  429. $custom_type_fields[$custom_type][$field] = search_api_list_nesting_level($info['real_type']);
  430. }
  431. }
  432. }
  433. if (empty($fields)) {
  434. throw new SearchApiException(t("Couldn't index values on '@name' index (no fields selected)", array('@name' => $this->name)));
  435. }
  436. // Mark all items that are rejected as indexed.
  437. $ret = array_keys($items);
  438. drupal_alter('search_api_index_items', $items, $this);
  439. if ($items) {
  440. $this->dataAlter($items);
  441. }
  442. $ret = array_diff($ret, array_keys($items));
  443. // Items that are rejected should also be deleted from the server.
  444. if ($ret) {
  445. $this->server()->deleteItems($ret, $this);
  446. }
  447. if (!$items) {
  448. return $ret;
  449. }
  450. $data = array();
  451. foreach ($items as $id => $item) {
  452. $data[$id] = search_api_extract_fields($this->entityWrapper($item), $fields);
  453. unset($items[$id]);
  454. foreach ($custom_type_fields as $type => $type_fields) {
  455. $info = search_api_get_data_type_info($type);
  456. if (isset($info['conversion callback']) && is_callable($info['conversion callback'])) {
  457. $callback = $info['conversion callback'];
  458. foreach ($type_fields as $field => $nesting_level) {
  459. if (isset($data[$id][$field]['value'])) {
  460. $value = $data[$id][$field]['value'];
  461. $original_type = $data[$id][$field]['original_type'];
  462. $data[$id][$field]['value'] = _search_api_convert_custom_type($callback, $value, $original_type, $type, $nesting_level);
  463. }
  464. }
  465. }
  466. }
  467. }
  468. $this->preprocessIndexItems($data);
  469. return array_merge($ret, $this->server()->indexItems($this, $data));
  470. }
  471. /**
  472. * Calls data alteration hooks for a set of items, according to the index
  473. * options.
  474. *
  475. * @param array $items
  476. * An array of items to be altered.
  477. *
  478. * @return SearchApiIndex
  479. * The called object.
  480. */
  481. public function dataAlter(array &$items) {
  482. // First, execute our own search_api_language data alteration.
  483. foreach ($items as &$item) {
  484. $item->search_api_language = isset($item->language) ? $item->language : LANGUAGE_NONE;
  485. }
  486. foreach ($this->getAlterCallbacks() as $callback) {
  487. $callback->alterItems($items);
  488. }
  489. return $this;
  490. }
  491. /**
  492. * Property info alter callback that adds the infos of the properties added by
  493. * data alter callbacks.
  494. *
  495. * @param EntityMetadataWrapper $wrapper
  496. * The wrapped data.
  497. * @param $property_info
  498. * The original property info.
  499. *
  500. * @return array
  501. * The altered property info.
  502. */
  503. public function propertyInfoAlter(EntityMetadataWrapper $wrapper, array $property_info) {
  504. if (entity_get_property_info($wrapper->type())) {
  505. // Overwrite the existing properties with the list of properties including
  506. // all fields regardless of the used bundle.
  507. $property_info['properties'] = entity_get_all_property_info($wrapper->type());
  508. }
  509. if (!isset($this->added_properties)) {
  510. $this->added_properties = array(
  511. 'search_api_language' => array(
  512. 'label' => t('Item language'),
  513. 'description' => t("A field added by the search framework to let components determine an item's language. Is always indexed."),
  514. 'type' => 'token',
  515. 'options list' => 'entity_metadata_language_list',
  516. ),
  517. );
  518. // We use the reverse order here so the hierarchy for overwriting property
  519. // infos is the same as for actually overwriting the properties.
  520. foreach (array_reverse($this->getAlterCallbacks()) as $callback) {
  521. $props = $callback->propertyInfo();
  522. if ($props) {
  523. $this->added_properties += $props;
  524. }
  525. }
  526. }
  527. // Let fields added by data-alter callbacks override default fields.
  528. $property_info['properties'] = array_merge($property_info['properties'], $this->added_properties);
  529. return $property_info;
  530. }
  531. /**
  532. * Loads all enabled data alterations for this index in proper order.
  533. *
  534. * @return array
  535. * All enabled callbacks for this index, as SearchApiAlterCallbackInterface
  536. * objects.
  537. */
  538. public function getAlterCallbacks() {
  539. if (isset($this->callbacks)) {
  540. return $this->callbacks;
  541. }
  542. $this->callbacks = array();
  543. if (empty($this->options['data_alter_callbacks'])) {
  544. return $this->callbacks;
  545. }
  546. $callback_settings = $this->options['data_alter_callbacks'];
  547. $infos = search_api_get_alter_callbacks();
  548. foreach ($callback_settings as $id => $settings) {
  549. if (empty($settings['status'])) {
  550. continue;
  551. }
  552. if (empty($infos[$id]) || !class_exists($infos[$id]['class'])) {
  553. watchdog('search_api', t('Undefined data alteration @class specified in index @name', array('@class' => $id, '@name' => $this->name)), NULL, WATCHDOG_WARNING);
  554. continue;
  555. }
  556. $class = $infos[$id]['class'];
  557. $callback = new $class($this, empty($settings['settings']) ? array() : $settings['settings']);
  558. if (!($callback instanceof SearchApiAlterCallbackInterface)) {
  559. watchdog('search_api', t('Unknown callback class @class specified for data alteration @name', array('@class' => $class, '@name' => $id)), NULL, WATCHDOG_WARNING);
  560. continue;
  561. }
  562. $this->callbacks[$id] = $callback;
  563. }
  564. return $this->callbacks;
  565. }
  566. /**
  567. * Loads all enabled processors for this index in proper order.
  568. *
  569. * @return array
  570. * All enabled processors for this index, as SearchApiProcessorInterface
  571. * objects.
  572. */
  573. public function getProcessors() {
  574. if (isset($this->processors)) {
  575. return $this->processors;
  576. }
  577. $this->processors = array();
  578. if (empty($this->options['processors'])) {
  579. return $this->processors;
  580. }
  581. $processor_settings = $this->options['processors'];
  582. $infos = search_api_get_processors();
  583. foreach ($processor_settings as $id => $settings) {
  584. if (empty($settings['status'])) {
  585. continue;
  586. }
  587. if (empty($infos[$id]) || !class_exists($infos[$id]['class'])) {
  588. watchdog('search_api', t('Undefined processor @class specified in index @name', array('@class' => $id, '@name' => $this->name)), NULL, WATCHDOG_WARNING);
  589. continue;
  590. }
  591. $class = $infos[$id]['class'];
  592. $processor = new $class($this, isset($settings['settings']) ? $settings['settings'] : array());
  593. if (!($processor instanceof SearchApiProcessorInterface)) {
  594. watchdog('search_api', t('Unknown processor class @class specified for processor @name', array('@class' => $class, '@name' => $id)), NULL, WATCHDOG_WARNING);
  595. continue;
  596. }
  597. $this->processors[$id] = $processor;
  598. }
  599. return $this->processors;
  600. }
  601. /**
  602. * Preprocess data items for indexing. Data added by data alter callbacks will
  603. * be available on the items.
  604. *
  605. * Typically, a preprocessor will execute its preprocessing (e.g. stemming,
  606. * n-grams, word splitting, stripping stop words, etc.) only on the items'
  607. * fulltext fields. Other fields should usually be left untouched.
  608. *
  609. * @param array $items
  610. * An array of items to be preprocessed for indexing.
  611. *
  612. * @return SearchApiIndex
  613. * The called object.
  614. */
  615. public function preprocessIndexItems(array &$items) {
  616. foreach ($this->getProcessors() as $processor) {
  617. $processor->preprocessIndexItems($items);
  618. }
  619. return $this;
  620. }
  621. /**
  622. * Preprocess a search query.
  623. *
  624. * The same applies as when preprocessing indexed items: typically, only the
  625. * fulltext search keys should be processed, queries on specific fields should
  626. * usually not be altered.
  627. *
  628. * @param SearchApiQuery $query
  629. * The object representing the query to be executed.
  630. *
  631. * @return SearchApiIndex
  632. * The called object.
  633. */
  634. public function preprocessSearchQuery(SearchApiQuery $query) {
  635. foreach ($this->getProcessors() as $processor) {
  636. $processor->preprocessSearchQuery($query);
  637. }
  638. return $this;
  639. }
  640. /**
  641. * Postprocess search results before display.
  642. *
  643. * If a class is used for both pre- and post-processing a search query, the
  644. * same object will be used for both calls (so preserving some data or state
  645. * locally is possible).
  646. *
  647. * @param array $response
  648. * An array containing the search results. See
  649. * SearchApiServiceInterface->search() for the detailed format.
  650. * @param SearchApiQuery $query
  651. * The object representing the executed query.
  652. *
  653. * @return SearchApiIndex
  654. * The called object.
  655. */
  656. public function postprocessSearchResults(array &$response, SearchApiQuery $query) {
  657. // Postprocessing is done in exactly the opposite direction than preprocessing.
  658. foreach (array_reverse($this->getProcessors()) as $processor) {
  659. $processor->postprocessSearchResults($response, $query);
  660. }
  661. return $this;
  662. }
  663. /**
  664. * Returns a list of all known fields for this index.
  665. *
  666. * @param $only_indexed (optional)
  667. * Return only indexed fields, not all known fields. Defaults to TRUE.
  668. * @param $get_additional (optional)
  669. * Return not only known/indexed fields, but also related entities whose
  670. * fields could additionally be added to the index.
  671. *
  672. * @return array
  673. * An array of all known fields for this index. Keys are the field
  674. * identifiers, the values are arrays for specifying the field settings. The
  675. * structure of those arrays looks like this:
  676. * - name: The human-readable name for the field.
  677. * - description: A description of the field, if available.
  678. * - indexed: Boolean indicating whether the field is indexed or not.
  679. * - type: The type set for this field. One of the types returned by
  680. * search_api_default_field_types().
  681. * - real_type: (optional) If a custom data type was selected for this
  682. * field, this type will be stored here, and "type" contain the fallback
  683. * default data type.
  684. * - boost: A boost value for terms found in this field during searches.
  685. * Usually only relevant for fulltext fields.
  686. * - entity_type (optional): If set, the type of this field is really an
  687. * entity. The "type" key will then contain "integer", meaning that
  688. * servers will ignore this and merely index the entity's ID. Components
  689. * displaying this field, though, are advised to use the entity label
  690. * instead of the ID.
  691. * If $get_additional is TRUE, this array is encapsulated in another
  692. * associative array, which contains the above array under the "fields" key,
  693. * and a list of related entities (field keys mapped to names) under the
  694. * "additional fields" key.
  695. */
  696. public function getFields($only_indexed = TRUE, $get_additional = FALSE) {
  697. $only_indexed = $only_indexed ? 1 : 0;
  698. $get_additional = $get_additional ? 1 : 0;
  699. // First, try the static cache and the persistent cache bin.
  700. if (empty($this->fields[$only_indexed][$get_additional])) {
  701. $cid = $this->getCacheId() . "-$only_indexed-$get_additional";
  702. $cache = cache_get($cid);
  703. if ($cache) {
  704. $this->fields[$only_indexed][$get_additional] = $cache->data;
  705. }
  706. }
  707. // Otherwise, we have to compute the result.
  708. if (empty($this->fields[$only_indexed][$get_additional])) {
  709. $fields = empty($this->options['fields']) ? array() : $this->options['fields'];
  710. $wrapper = $this->entityWrapper();
  711. $additional = array();
  712. $entity_types = entity_get_info();
  713. // First we need all already added prefixes.
  714. $added = ($only_indexed || empty($this->options['additional fields'])) ? array() : $this->options['additional fields'];
  715. foreach (array_keys($fields) as $key) {
  716. $len = strlen($key) + 1;
  717. $pos = $len;
  718. // The third parameter ($offset) to strrpos has rather weird behaviour,
  719. // necessitating this rather awkward code. It will iterate over all
  720. // prefixes of each field, beginning with the longest, adding all of them
  721. // to $added until one is encountered that was already added (which means
  722. // all shorter ones will have already been added, too).
  723. while ($pos = strrpos($key, ':', $pos - $len)) {
  724. $prefix = substr($key, 0, $pos);
  725. if (isset($added[$prefix])) {
  726. break;
  727. }
  728. $added[$prefix] = $prefix;
  729. }
  730. }
  731. // Then we walk through all properties and look if they are already
  732. // contained in one of the arrays.
  733. // Since this uses an iterative instead of a recursive approach, it is a bit
  734. // complicated, with three arrays tracking the current depth.
  735. // A wrapper for a specific field name prefix, e.g. 'user:' mapped to the user wrapper
  736. $wrappers = array('' => $wrapper);
  737. // Display names for the prefixes
  738. $prefix_names = array('' => '');
  739. // The list nesting level for entities with a certain prefix
  740. $nesting_levels = array('' => 0);
  741. $types = search_api_default_field_types();
  742. $flat = array();
  743. while ($wrappers) {
  744. foreach ($wrappers as $prefix => $wrapper) {
  745. $prefix_name = $prefix_names[$prefix];
  746. // Deal with lists of entities.
  747. $nesting_level = $nesting_levels[$prefix];
  748. $type_prefix = str_repeat('list<', $nesting_level);
  749. $type_suffix = str_repeat('>', $nesting_level);
  750. if ($nesting_level) {
  751. $info = $wrapper->info();
  752. // The real nesting level of the wrapper, not the accumulated one.
  753. $level = search_api_list_nesting_level($info['type']);
  754. for ($i = 0; $i < $level; ++$i) {
  755. $wrapper = $wrapper[0];
  756. }
  757. }
  758. // Now look at all properties.
  759. foreach ($wrapper as $property => $value) {
  760. $info = $value->info();
  761. // We hide the complexity of multi-valued types from the user here.
  762. $type = search_api_extract_inner_type($info['type']);
  763. // Treat Entity API type "token" as our "string" type.
  764. // Also let text fields with limited options be of type "string" by default.
  765. if ($type == 'token' || ($type == 'text' && !empty($info['options list']))) {
  766. // Inner type is changed to "string".
  767. $type = 'string';
  768. // Set the field type accordingly.
  769. $info['type'] = search_api_nest_type('string', $info['type']);
  770. }
  771. $info['type'] = $type_prefix . $info['type'] . $type_suffix;
  772. $key = $prefix . $property;
  773. if ((isset($types[$type]) || isset($entity_types[$type])) && (!$only_indexed || !empty($fields[$key]))) {
  774. if (!empty($fields[$key])) {
  775. // This field is already known in the index configuration.
  776. $flat[$key] = $fields[$key] + array(
  777. 'name' => $prefix_name . $info['label'],
  778. 'description' => empty($info['description']) ? NULL : $info['description'],
  779. 'boost' => '1.0',
  780. 'indexed' => TRUE,
  781. );
  782. // Update the type and its nesting level for non-entity properties.
  783. if (!isset($entity_types[$type])) {
  784. $flat[$key]['type'] = search_api_nest_type(search_api_extract_inner_type($flat[$key]['type']), $info['type']);
  785. if (isset($flat[$key]['real_type'])) {
  786. $real_type = search_api_extract_inner_type($flat[$key]['real_type']);
  787. $flat[$key]['real_type'] = search_api_nest_type($real_type, $info['type']);
  788. }
  789. }
  790. }
  791. else {
  792. $flat[$key] = array(
  793. 'name' => $prefix_name . $info['label'],
  794. 'description' => empty($info['description']) ? NULL : $info['description'],
  795. 'type' => $info['type'],
  796. 'boost' => '1.0',
  797. 'indexed' => FALSE,
  798. );
  799. }
  800. if (isset($entity_types[$type])) {
  801. $base_type = isset($entity_types[$type]['entity keys']['name']) ? 'string' : 'integer';
  802. $flat[$key]['type'] = search_api_nest_type($base_type, $info['type']);
  803. $flat[$key]['entity_type'] = $type;
  804. }
  805. }
  806. if (empty($types[$type])) {
  807. if (isset($added[$key])) {
  808. // Visit this entity/struct in a later iteration.
  809. $wrappers[$key . ':'] = $value;
  810. $prefix_names[$key . ':'] = $prefix_name . $info['label'] . ' » ';
  811. $nesting_levels[$key . ':'] = search_api_list_nesting_level($info['type']);
  812. }
  813. else {
  814. $name = $prefix_name . $info['label'];
  815. // Add machine names to discern fields with identical labels.
  816. if (isset($used_names[$name])) {
  817. if ($used_names[$name] !== FALSE) {
  818. $additional[$used_names[$name]] .= ' [' . $used_names[$name] . ']';
  819. $used_names[$name] = FALSE;
  820. }
  821. $name .= ' [' . $key . ']';
  822. }
  823. $additional[$key] = $name;
  824. $used_names[$name] = $key;
  825. }
  826. }
  827. }
  828. unset($wrappers[$prefix]);
  829. }
  830. }
  831. if (!$get_additional) {
  832. $this->fields[$only_indexed][$get_additional] = $flat;
  833. }
  834. else {
  835. $options = array();
  836. $options['fields'] = $flat;
  837. $options['additional fields'] = $additional;
  838. $this->fields[$only_indexed][$get_additional] = $options;
  839. }
  840. cache_set($cid, $this->fields[$only_indexed][$get_additional]);
  841. }
  842. return $this->fields[$only_indexed][$get_additional];
  843. }
  844. /**
  845. * Convenience method for getting all of this index's fulltext fields.
  846. *
  847. * @param boolean $only_indexed
  848. * If set to TRUE, only the indexed fulltext fields will be returned.
  849. *
  850. * @return array
  851. * An array containing all (or all indexed) fulltext fields defined for this
  852. * index.
  853. */
  854. public function getFulltextFields($only_indexed = TRUE) {
  855. $i = $only_indexed ? 1 : 0;
  856. if (!isset($this->fulltext_fields[$i])) {
  857. $this->fulltext_fields[$i] = array();
  858. $fields = $only_indexed ? $this->options['fields'] : $this->getFields(FALSE);
  859. foreach ($fields as $key => $field) {
  860. if (search_api_is_text_type($field['type'])) {
  861. $this->fulltext_fields[$i][] = $key;
  862. }
  863. }
  864. }
  865. return $this->fulltext_fields[$i];
  866. }
  867. /**
  868. * Get the cache ID prefix used for this index's caches.
  869. *
  870. * @param $type
  871. * The type of cache. Currently only "fields" is used.
  872. *
  873. * @return
  874. * The cache ID (prefix) for this index's caches.
  875. */
  876. public function getCacheId($type = 'fields') {
  877. return 'search_api:index-' . $this->machine_name . '--' . $type;
  878. }
  879. /**
  880. * Helper function for creating an entity metadata wrapper appropriate for
  881. * this index.
  882. *
  883. * @param $item
  884. * Unless NULL, an item of this index's item type which should be wrapped.
  885. * @param $alter
  886. * Whether to apply the index's active data alterations on the property
  887. * information used. To also apply the data alteration to the wrapped item,
  888. * execute SearchApiIndex::dataAlter() on it before calling this method.
  889. *
  890. * @return EntityMetadataWrapper
  891. * A wrapper for the item type of this index, optionally loaded with the
  892. * given data and having additional fields according to the data alterations
  893. * of this index (if $alter wasn't set to FALSE).
  894. */
  895. public function entityWrapper($item = NULL, $alter = TRUE) {
  896. try {
  897. $info['property info alter'] = $alter ? array($this, 'propertyInfoAlter') : '_search_api_wrapper_add_all_properties';
  898. $info['property defaults']['property info alter'] = '_search_api_wrapper_add_all_properties';
  899. return $this->datasource()->getMetadataWrapper($item, $info);
  900. }
  901. catch (SearchApiException $e) {
  902. watchdog_exception('search_api', $e);
  903. return entity_metadata_wrapper($this->item_type);
  904. }
  905. }
  906. /**
  907. * Helper method to load items from the type lying on this index.
  908. *
  909. * @param array $ids
  910. * The IDs of the items to load.
  911. *
  912. * @return array
  913. * The requested items, as loaded by the data source.
  914. *
  915. * @see SearchApiDataSourceControllerInterface::loadItems()
  916. */
  917. public function loadItems(array $ids) {
  918. try {
  919. return $this->datasource()->loadItems($ids);
  920. }
  921. catch (SearchApiException $e) {
  922. watchdog_exception('search_api', $e);
  923. return array();
  924. }
  925. }
  926. /**
  927. * Reset internal caches.
  928. *
  929. * Should be used when things like fields or data alterations change to avoid
  930. * using stale data.
  931. */
  932. public function resetCaches() {
  933. cache_clear_all($this->getCacheId(''), 'cache', TRUE);
  934. $this->datasource = NULL;
  935. $this->server_object = NULL;
  936. $this->callbacks = NULL;
  937. $this->processors = NULL;
  938. $this->added_properties = NULL;
  939. $this->fields = array();
  940. $this->fulltext_fields = array();
  941. }
  942. }