index_entity.inc 34 KB

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