index_entity.inc 32 KB

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