index_entity.inc 34 KB

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