index_entity.inc 32 KB

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