service.inc 88 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499
  1. <?php
  2. /**
  3. * Search service class using Solr server.
  4. */
  5. class SearchApiSolrService extends SearchApiAbstractService {
  6. /**
  7. * The date format that Solr uses, in PHP date() syntax.
  8. */
  9. const SOLR_DATE_FORMAT = 'Y-m-d\TH:i:s\Z';
  10. /**
  11. * The connection class used by this service.
  12. *
  13. * Must implement SearchApiSolrConnectionInterface.
  14. *
  15. * @var string
  16. */
  17. protected $connection_class = 'SearchApiSolrConnection';
  18. /**
  19. * A connection to the Solr server.
  20. *
  21. * @var SearchApiSolrConnectionInterface
  22. */
  23. protected $solr;
  24. /**
  25. * Static cache for getFieldNames().
  26. *
  27. * @var array
  28. */
  29. protected $fieldNames = array();
  30. /**
  31. * Metadata describing fields on the Solr/Lucene index.
  32. *
  33. * @see SearchApiSolrService::getFields().
  34. *
  35. * @var array
  36. */
  37. protected $fields;
  38. /**
  39. * Saves whether a commit operation was already scheduled for this server.
  40. *
  41. * @var bool
  42. */
  43. protected $commitScheduled = FALSE;
  44. /**
  45. * Request handler to use for this search query.
  46. *
  47. * @var string
  48. */
  49. protected $request_handler = NULL;
  50. /**
  51. * {@inheritdoc}
  52. */
  53. public function configurationForm(array $form, array &$form_state) {
  54. if ($this->options) {
  55. // Editing this server
  56. $form['server_description'] = array(
  57. '#type' => 'item',
  58. '#title' => t('Solr server URI'),
  59. '#description' => $this->getServerLink(),
  60. );
  61. }
  62. $options = $this->options + array(
  63. 'scheme' => 'http',
  64. 'host' => 'localhost',
  65. 'port' => '8983',
  66. 'path' => '/solr',
  67. 'http_user' => '',
  68. 'http_pass' => '',
  69. 'excerpt' => FALSE,
  70. 'retrieve_data' => FALSE,
  71. 'highlight_data' => FALSE,
  72. 'skip_schema_check' => FALSE,
  73. 'log_query' => FALSE,
  74. 'log_response' => FALSE,
  75. 'commits_disabled' => FALSE,
  76. 'solr_version' => '',
  77. 'http_method' => 'AUTO',
  78. // Default to TRUE for new servers, but to FALSE for existing ones.
  79. 'clean_ids' => $this->options ? FALSE : TRUE,
  80. 'site_hash' => $this->options ? FALSE : TRUE,
  81. 'autocorrect_spell' => TRUE,
  82. 'autocorrect_suggest_words' => TRUE,
  83. );
  84. if (!$options['clean_ids']) {
  85. if (module_exists('advanced_help')) {
  86. $variables['@url'] = url('help/search_api_solr/README.txt');
  87. }
  88. else {
  89. $variables['@url'] = url(drupal_get_path('module', 'search_api_solr') . '/README.txt');
  90. }
  91. $description = t('Change Solr field names to be more compatible with advanced features. Doing this leads to re-indexing of all indexes on this server. See <a href="@url">README.txt</a> for details.', $variables);
  92. $form['clean_ids_form'] = array(
  93. '#type' => 'fieldset',
  94. '#title' => t('Clean field identifiers'),
  95. '#description' => $description,
  96. '#collapsible' => TRUE,
  97. );
  98. $form['clean_ids_form']['submit'] = array(
  99. '#type' => 'submit',
  100. '#value' => t('Switch to clean field identifiers'),
  101. '#submit' => array('_search_api_solr_switch_to_clean_ids'),
  102. );
  103. }
  104. $form['clean_ids'] = array(
  105. '#type' => 'value',
  106. '#value' => $options['clean_ids'],
  107. );
  108. if (!$options['site_hash']) {
  109. $description = t('If you want to index content from multiple sites on a single Solr server, you should enable the multi-site compatibility here. Note, however, that this will completely clear all search indexes (from this site) lying on this server. All content will have to be re-indexed.');
  110. $form['site_hash_form'] = array(
  111. '#type' => 'fieldset',
  112. '#title' => t('Multi-site compatibility'),
  113. '#description' => $description,
  114. '#collapsible' => TRUE,
  115. );
  116. $form['site_hash_form']['submit'] = array(
  117. '#type' => 'submit',
  118. '#value' => t('Turn on multi-site compatibility and clear all indexes'),
  119. '#submit' => array('_search_api_solr_switch_to_site_hash'),
  120. );
  121. }
  122. $form['site_hash'] = array(
  123. '#type' => 'value',
  124. '#value' => $options['site_hash'],
  125. );
  126. $form['scheme'] = array(
  127. '#type' => 'select',
  128. '#title' => t('HTTP protocol'),
  129. '#description' => t('The HTTP protocol to use for sending queries.'),
  130. '#default_value' => $options['scheme'],
  131. '#options' => array(
  132. 'http' => 'http',
  133. 'https' => 'https',
  134. ),
  135. );
  136. $form['host'] = array(
  137. '#type' => 'textfield',
  138. '#title' => t('Solr host'),
  139. '#description' => t('The host name or IP of your Solr server, e.g. <code>localhost</code> or <code>www.example.com</code>.'),
  140. '#default_value' => $options['host'],
  141. '#required' => TRUE,
  142. );
  143. $form['port'] = array(
  144. '#type' => 'textfield',
  145. '#title' => t('Solr port'),
  146. '#description' => t('The Jetty example server is at port 8983, while Tomcat uses 8080 by default.'),
  147. '#default_value' => $options['port'],
  148. '#required' => TRUE,
  149. );
  150. $form['path'] = array(
  151. '#type' => 'textfield',
  152. '#title' => t('Solr path'),
  153. '#description' => t('The path that identifies the Solr instance to use on the server. (For Solr versions 4 and above, this should include the name of the core to use.)'),
  154. '#default_value' => $options['path'],
  155. );
  156. $form['http'] = array(
  157. '#type' => 'fieldset',
  158. '#title' => t('Basic HTTP authentication'),
  159. '#description' => t('If your Solr server is protected by basic HTTP authentication, enter the login data here.'),
  160. '#collapsible' => TRUE,
  161. '#collapsed' => empty($options['http_user']),
  162. );
  163. $form['http']['http_user'] = array(
  164. '#type' => 'textfield',
  165. '#title' => t('Username'),
  166. '#default_value' => $options['http_user'],
  167. // This prefix with no-op text and password field will keep most browsers
  168. // from autocompleting these fields, which is hardly ever what the user
  169. // wants.
  170. '#prefix' => '<input type="text" style="display:none" /><input type="password" style="display:none" />',
  171. );
  172. $form['http']['http_pass'] = array(
  173. '#type' => 'password',
  174. '#title' => t('Password'),
  175. '#description' => t('If this field is left blank and the HTTP username is filled out, the current password will not be changed.'),
  176. );
  177. $form['advanced'] = array(
  178. '#type' => 'fieldset',
  179. '#title' => t('Advanced'),
  180. '#collapsible' => TRUE,
  181. '#collapsed' => TRUE,
  182. );
  183. $form['advanced']['excerpt'] = array(
  184. '#type' => 'checkbox',
  185. '#title' => t('Return an excerpt for all results'),
  186. '#description' => t("If search keywords are given, use Solr's capabilities to create a highlighted search excerpt for each result. " .
  187. 'Whether the excerpts will actually be displayed depends on the settings of the search, though.'),
  188. '#default_value' => $options['excerpt'],
  189. );
  190. $form['advanced']['retrieve_data'] = array(
  191. '#type' => 'checkbox',
  192. '#title' => t('Retrieve result data from Solr'),
  193. '#description' => t('When checked, result data will be retrieved directly from the Solr server. ' .
  194. 'This might make item loads unnecessary. Only indexed fields can be retrieved. ' .
  195. 'Note also that the returned field data might not always be correct, due to preprocessing and caching issues.'),
  196. '#default_value' => $options['retrieve_data'],
  197. );
  198. $form['advanced']['highlight_data'] = array(
  199. '#type' => 'checkbox',
  200. '#title' => t('Highlight retrieved data'),
  201. '#description' => t('When retrieving result data from the Solr server, try to highlight the search terms in the returned fulltext fields. <strong>Note:</strong> Do not use the "Highlighting" processor for the index together with this option – use one or the other.'),
  202. '#default_value' => $options['highlight_data'],
  203. );
  204. // Highlighting retrieved data only makes sense when we retrieve data.
  205. // (Actually, internally it doesn't really matter. However, from a user's
  206. // perspective, having to check both probably makes sense.)
  207. $form['advanced']['highlight_data']['#states']['invisible']
  208. [':input[name="options[form][advanced][retrieve_data]"]']['checked'] = FALSE;
  209. $form['advanced']['skip_schema_check'] = array(
  210. '#type' => 'checkbox',
  211. '#title' => t('Skip schema verification'),
  212. '#description' => t('Skip the automatic check for schema-compatibility. Use this override if you are seeing an error-message about an incompatible schema.xml configuration file, and you are sure the configuration is compatible.'),
  213. '#default_value' => $options['skip_schema_check'],
  214. );
  215. $form['advanced']['solr_version'] = array(
  216. '#type' => 'select',
  217. '#title' => t('Solr version override'),
  218. '#description' => t('Specify the Solr version manually in case it cannot be retrieved automatically. The version can be found in the Solr admin interface under "Solr Specification Version" or "solr-spec".'),
  219. '#options' => array(
  220. '' => t('Determine automatically'),
  221. '3' => '3.x',
  222. '4' => '4.x',
  223. '5' => '5.x',
  224. ),
  225. '#default_value' => $options['solr_version'],
  226. );
  227. $form['advanced']['http_method'] = array(
  228. '#type' => 'select',
  229. '#title' => t('HTTP method'),
  230. '#description' => t('The HTTP method to use for sending queries. GET will often fail with larger queries, while POST should not be cached. AUTO will use GET when possible, and POST for queries that are too large.'),
  231. '#default_value' => $options['http_method'],
  232. '#options' => array(
  233. 'AUTO' => t('AUTO'),
  234. 'POST' => 'POST',
  235. 'GET' => 'GET',
  236. ),
  237. );
  238. $form['advanced']['log_query'] = array(
  239. '#type' => 'checkbox',
  240. '#title' => t('Log search requests'),
  241. '#description' => t('Log all outgoing Solr search requests.'),
  242. '#default_value' => $options['log_query'],
  243. );
  244. $form['advanced']['log_response'] = array(
  245. '#type' => 'checkbox',
  246. '#title' => t('Log search results'),
  247. '#description' => t('Log all search result responses received from Solr. NOTE: This may slow down your site since all response data (including possible retrieved data) will be saved in the Drupal log.'),
  248. '#default_value' => $options['log_response'],
  249. );
  250. $form['advanced']['commits_disabled'] = array(
  251. '#type' => 'checkbox',
  252. '#title' => t('Disable explicit committing'),
  253. '#description' => t('Do not send any commit commands to the Solr server.'),
  254. '#default_value' => $options['commits_disabled'],
  255. );
  256. if (module_exists('search_api_autocomplete')) {
  257. $form['advanced']['autocomplete'] = array(
  258. '#type' => 'fieldset',
  259. '#title' => t('Autocomplete'),
  260. '#collapsible' => TRUE,
  261. '#collapsed' => TRUE,
  262. );
  263. $form['advanced']['autocomplete']['autocorrect_spell'] = array(
  264. '#type' => 'checkbox',
  265. '#title' => t('Use spellcheck for autocomplete suggestions'),
  266. '#description' => t('If activated, spellcheck suggestions ("Did you mean") will be included in the autocomplete suggestions. Since the used dictionary contains words from all indexes, this might lead to leaking of sensitive data, depending on your setup.'),
  267. '#default_value' => $options['autocorrect_spell'],
  268. );
  269. $form['advanced']['autocomplete']['autocorrect_suggest_words'] = array(
  270. '#type' => 'checkbox',
  271. '#title' => t('Suggest additional words'),
  272. '#description' => t('If activated and the user enters a complete word, Solr will suggest additional words the user wants to search, which are often found (not searched!) together. This has been known to lead to strange results in some configurations – if you see inappropriate additional-word suggestions, you might want to deactivate this option.'),
  273. '#default_value' => $options['autocorrect_suggest_words'],
  274. );
  275. }
  276. return $form;
  277. }
  278. /**
  279. * {@inheritdoc}
  280. */
  281. public function configurationFormValidate(array $form, array &$values, array &$form_state) {
  282. if (isset($values['port']) && (!is_numeric($values['port']) || $values['port'] < 0 || $values['port'] > 65535)) {
  283. form_error($form['port'], t('The port has to be an integer between 0 and 65535.'));
  284. }
  285. }
  286. /**
  287. * {@inheritdoc}
  288. */
  289. public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
  290. // Since the form is nested into another, we can't simply use #parents for
  291. // doing this array restructuring magic. (At least not without creating an
  292. // unnecessary dependency on internal implementation.)
  293. $values += $values['http'];
  294. $values += $values['advanced'];
  295. $values += !empty($values['autocomplete']) ? $values['autocomplete'] : array();
  296. unset($values['http'], $values['advanced'], $values['autocomplete']);
  297. // Highlighting retrieved data only makes sense when we retrieve data.
  298. $values['highlight_data'] &= $values['retrieve_data'];
  299. // For password fields, there is no default value, they're empty by default.
  300. // Therefore we ignore empty submissions if the user didn't change either.
  301. if ($values['http_pass'] === ''
  302. && isset($this->options['http_user'])
  303. && $values['http_user'] === $this->options['http_user']) {
  304. $values['http_pass'] = $this->options['http_pass'];
  305. }
  306. parent::configurationFormSubmit($form, $values, $form_state);
  307. }
  308. /**
  309. * {@inheritdoc}
  310. */
  311. public function supportsFeature($feature) {
  312. // First, check the features we always support.
  313. $supported = drupal_map_assoc(array(
  314. 'search_api_autocomplete',
  315. 'search_api_facets',
  316. 'search_api_facets_operator_or',
  317. 'search_api_grouping',
  318. 'search_api_mlt',
  319. 'search_api_multi',
  320. 'search_api_service_extra',
  321. 'search_api_spellcheck',
  322. 'search_api_data_type_location',
  323. 'search_api_data_type_geohash',
  324. 'search_api_random_sort',
  325. ));
  326. if (isset($supported[$feature])) {
  327. return TRUE;
  328. }
  329. // If it is a custom data type, maybe we support it automatically via
  330. // search_api_solr_hook_search_api_data_type_info().
  331. if (substr($feature, 0, 21) != 'search_api_data_type_') {
  332. return FALSE;
  333. }
  334. $type = substr($feature, 21);
  335. $type = search_api_get_data_type_info($type);
  336. // We only support it if the "prefix" key is set.
  337. return $type && !empty($type['prefix']);
  338. }
  339. /**
  340. * Overrides SearchApiAbstractService::viewSettings().
  341. *
  342. * Returns an empty string since information is instead added via
  343. * getExtraInformation().
  344. */
  345. public function viewSettings() {
  346. return '';
  347. }
  348. /**
  349. * {@inheritdoc}
  350. */
  351. public function getExtraInformation() {
  352. $info = array();
  353. $info[] = array(
  354. 'label' => t('Solr server URI'),
  355. 'info' => $this->getServerLink(),
  356. );
  357. if ($this->options['http_user']) {
  358. $vars = array(
  359. '@user' => $this->options['http_user'],
  360. '@pass' => str_repeat('*', strlen($this->options['http_pass'])),
  361. );
  362. $http = t('Username: @user; Password: @pass', $vars);
  363. $info[] = array(
  364. 'label' => t('Basic HTTP authentication'),
  365. 'info' => $http,
  366. );
  367. }
  368. if ($this->server->enabled) {
  369. // If the server is enabled, check whether Solr can be reached.
  370. $ping = $this->ping();
  371. if ($ping) {
  372. $msg = t('The Solr server could be reached (latency: @millisecs ms).', array('@millisecs' => $ping * 1000));
  373. }
  374. else {
  375. $msg = t('The Solr server could not be reached. Further data is therefore unavailable.');
  376. }
  377. $info[] = array(
  378. 'label' => t('Connection'),
  379. 'info' => $msg,
  380. 'status' => $ping ? 'ok' : 'error',
  381. );
  382. if ($ping) {
  383. try {
  384. // If Solr can be reached, provide more information. This isn't done
  385. // often (only when an admin views the server details), so we clear the
  386. // cache to get the current data.
  387. $this->connect();
  388. $this->solr->clearCache();
  389. $data = $this->solr->getLuke();
  390. if (isset($data->index->numDocs)) {
  391. // Collect the stats
  392. $stats_summary = $this->solr->getStatsSummary();
  393. $pending_msg = $stats_summary['@pending_docs'] ? t('(@pending_docs sent but not yet processed)', $stats_summary) : '';
  394. $index_msg = $stats_summary['@index_size'] ? t('(@index_size on disk)', $stats_summary) : '';
  395. $indexed_message = t('@num items !pending !index_msg', array(
  396. '@num' => $data->index->numDocs,
  397. '!pending' => $pending_msg,
  398. '!index_msg' => $index_msg,
  399. ));
  400. $info[] = array(
  401. 'label' => t('Indexed'),
  402. 'info' => $indexed_message,
  403. );
  404. if (!empty($stats_summary['@deletes_total'])) {
  405. $info[] = array(
  406. 'label' => t('Pending Deletions'),
  407. 'info' => $stats_summary['@deletes_total'],
  408. );
  409. }
  410. $info[] = array(
  411. 'label' => t('Delay'),
  412. 'info' => t('@autocommit_time before updates are processed.', $stats_summary),
  413. );
  414. $status = 'ok';
  415. if (empty($this->options['skip_schema_check'])) {
  416. if (substr($stats_summary['@schema_version'], 0, 10) == 'search-api') {
  417. drupal_set_message(t('Your schema.xml version is too old. Please replace all configuration files with the ones packaged with this module and re-index you data.'), 'error');
  418. $status = 'error';
  419. }
  420. elseif (substr($stats_summary['@schema_version'], 0, 9) != 'drupal-4.') {
  421. $variables['@url'] = url('https://www.drupal.org/node/1999310');
  422. $message = t('You are using an incompatible schema.xml configuration file. Please follow the instructions in <a href="@url">the handbook</a> for setting up Solr.', $variables);
  423. drupal_set_message($message, 'error');
  424. $status = 'error';
  425. }
  426. }
  427. $info[] = array(
  428. 'label' => t('Schema'),
  429. 'info' => $stats_summary['@schema_version'],
  430. 'status' => $status,
  431. );
  432. if (!empty($stats_summary['@core_name'])) {
  433. $info[] = array(
  434. 'label' => t('Solr Core Name'),
  435. 'info' => $stats_summary['@core_name'],
  436. );
  437. }
  438. }
  439. }
  440. catch (SearchApiException $e) {
  441. $info[] = array(
  442. 'label' => t('Additional information'),
  443. 'info' => t('An error occurred while trying to retrieve additional information from the Solr server: @msg.', array('@msg' => $e->getMessage())),
  444. 'status' => 'error',
  445. );
  446. }
  447. }
  448. }
  449. return $info;
  450. }
  451. /**
  452. * Returns a link to the Solr server, if the necessary options are set.
  453. */
  454. public function getServerLink() {
  455. if (!$this->options) {
  456. return '';
  457. }
  458. $host = $this->options['host'];
  459. if ($host == 'localhost' && !empty($_SERVER['SERVER_NAME'])) {
  460. $host = $_SERVER['SERVER_NAME'];
  461. }
  462. $url = $this->options['scheme'] . '://' . $host . ':' . $this->options['port'] . $this->options['path'];
  463. return l($url, $url);
  464. }
  465. /**
  466. * Create a connection to the Solr server as configured in $this->options.
  467. */
  468. protected function connect() {
  469. if (!$this->solr) {
  470. if (!class_exists($this->connection_class)) {
  471. throw new SearchApiException(t('Invalid class @class set as Solr connection class.', array('@class' => $this->connection_class)));
  472. }
  473. $options = $this->options + array('server' => $this->server->machine_name);
  474. $this->solr = new $this->connection_class($options);
  475. if (!($this->solr instanceof SearchApiSolrConnectionInterface)) {
  476. $this->solr = NULL;
  477. throw new SearchApiException(t('Invalid class @class set as Solr connection class.', array('@class' => $this->connection_class)));
  478. }
  479. }
  480. }
  481. /**
  482. * {@inheritdoc}
  483. */
  484. public function addIndex(SearchApiIndex $index) {
  485. if (module_exists('search_api_multi') && module_exists('search_api_views')) {
  486. views_invalidate_cache();
  487. }
  488. }
  489. /**
  490. * {@inheritdoc}
  491. */
  492. public function fieldsUpdated(SearchApiIndex $index) {
  493. if (module_exists('search_api_multi') && module_exists('search_api_views')) {
  494. views_invalidate_cache();
  495. }
  496. // Find out if anything changed enough to need re-indexing.
  497. $old_fields = isset($index->original->options['fields']) ? $index->original->options['fields'] : array();
  498. $new_fields = isset($index->options['fields']) ? $index->options['fields'] : array();
  499. if (!$old_fields && !$new_fields) {
  500. return FALSE;
  501. }
  502. if (array_diff_key($old_fields, $new_fields) || array_diff_key($new_fields, $old_fields)) {
  503. return TRUE;
  504. }
  505. $old_field_names = $this->getFieldNames($index->original, TRUE);
  506. $new_field_names = $this->getFieldNames($index, TRUE);
  507. return $old_field_names != $new_field_names;
  508. }
  509. /**
  510. * {@inheritdoc}
  511. */
  512. public function removeIndex($index) {
  513. if (module_exists('search_api_multi') && module_exists('search_api_views')) {
  514. views_invalidate_cache();
  515. }
  516. parent::removeIndex($index);
  517. }
  518. /**
  519. * {@inheritdoc}
  520. */
  521. public function indexItems(SearchApiIndex $index, array $items) {
  522. $documents = array();
  523. $ret = array();
  524. $index_id = $this->getIndexId($index->machine_name);
  525. $fields = $this->getFieldNames($index);
  526. $languages = language_list();
  527. $base_urls = array();
  528. foreach ($items as $id => $item) {
  529. $doc = new SearchApiSolrDocument();
  530. $doc->setField('id', $this->createId($index_id, $id));
  531. $doc->setField('index_id', $index_id);
  532. $doc->setField('item_id', $id);
  533. // If multi-site compatibility is enabled, add the site hash and
  534. // language-specific base URL.
  535. if (!empty($this->options['site_hash'])) {
  536. $doc->setField('hash', search_api_solr_site_hash());
  537. $lang = $item['search_api_language']['value'];
  538. if (empty($base_urls[$lang])) {
  539. $url_options = array('absolute' => TRUE);
  540. if (isset($languages[$lang])) {
  541. $url_options['language'] = $languages[$lang];
  542. }
  543. $base_urls[$lang] = url(NULL, $url_options);
  544. }
  545. $doc->setField('site', $base_urls[$lang]);
  546. }
  547. // Now add all fields contained in the item, with dynamic fields. Also,
  548. // gather the contents of all text fields to also add them to "content".
  549. $text_content = array();
  550. foreach ($item as $key => $field) {
  551. // If the field is not known for the index, something weird has
  552. // happened. We refuse to index the items and hope that the others are
  553. // OK.
  554. if (!isset($fields[$key])) {
  555. $type = search_api_get_item_type_info($index->item_type);
  556. $vars = array(
  557. '@field' => $key,
  558. '@type' => $type ? $type['name'] : $index->item_type,
  559. '@id' => $id,
  560. );
  561. watchdog('search_api_solr', 'Error while indexing: Unknown field @field set for @type with ID @id.', $vars, WATCHDOG_WARNING);
  562. $doc = NULL;
  563. break;
  564. }
  565. $text_content[] = $this->addIndexField($doc, $fields[$key], $field['value'], $field['type']);
  566. }
  567. $doc->setField('content', implode("\n\n", array_filter($text_content)));
  568. if ($doc) {
  569. $documents[] = $doc;
  570. $ret[] = $id;
  571. }
  572. }
  573. // Let other modules alter documents before sending them to solr.
  574. drupal_alter('search_api_solr_documents', $documents, $index, $items);
  575. $this->alterSolrDocuments($documents, $index, $items);
  576. if (!$documents) {
  577. return array();
  578. }
  579. try {
  580. $this->connect();
  581. $this->solr->addDocuments($documents);
  582. if (!empty($index->options['index_directly'])) {
  583. $this->scheduleCommit();
  584. }
  585. return $ret;
  586. }
  587. catch (SearchApiException $e) {
  588. watchdog_exception('search_api_solr', $e, "%type while indexing: !message in %function (line %line of %file).");
  589. }
  590. return array();
  591. }
  592. /**
  593. * Creates an ID used as the unique identifier at the Solr server.
  594. *
  595. * This has to consist of both index and item ID. Optionally, the site hash is
  596. * also included.
  597. *
  598. * @see search_api_solr_site_hash()
  599. */
  600. protected function createId($index_id, $item_id) {
  601. $site_hash = !empty($this->options['site_hash']) ? search_api_solr_site_hash() . '-' : '';
  602. return "$site_hash$index_id-$item_id";
  603. }
  604. /**
  605. * Create a list of all indexed field names mapped to their Solr field names.
  606. *
  607. * The special fields "search_api_id" and "search_api_relevance" are also
  608. * included. Any Solr fields that exist on search results are mapped back to
  609. * to their local field names in the final result set.
  610. *
  611. * @see SearchApiSolrService::search()
  612. */
  613. public function getFieldNames(SearchApiIndex $index, $reset = FALSE) {
  614. if (!isset($this->fieldNames[$index->machine_name]) || $reset) {
  615. // This array maps "local property name" => "solr doc property name".
  616. $ret = array(
  617. 'search_api_id' => 'item_id',
  618. 'search_api_relevance' => 'score',
  619. 'search_api_random' => 'random',
  620. );
  621. // Add the names of any fields configured on the index.
  622. $fields = (isset($index->options['fields']) ? $index->options['fields'] : array());
  623. foreach ($fields as $key => $field) {
  624. // Generate a field name; this corresponds with naming conventions in
  625. // our schema.xml
  626. $type = $field['type'];
  627. // Use the real type of the field if the server supports this type.
  628. if (isset($field['real_type'])) {
  629. $custom_type = search_api_extract_inner_type($field['real_type']);
  630. if ($this->supportsFeature('search_api_data_type_' . $custom_type)) {
  631. $type = $field['real_type'];
  632. }
  633. }
  634. $inner_type = search_api_extract_inner_type($type);
  635. $type_info = search_api_solr_get_data_type_info($inner_type);
  636. $pref = isset($type_info['prefix']) ? $type_info['prefix']: '';
  637. if (empty($type_info['always multiValued'])) {
  638. $pref .= ($type == $inner_type) ? 's' : 'm';
  639. }
  640. if (!empty($this->options['clean_ids'])) {
  641. $name = $pref . '_' . str_replace(':', '$', $key);
  642. }
  643. else {
  644. $name = $pref . '_' . $key;
  645. }
  646. $ret[$key] = $name;
  647. }
  648. // Let modules adjust the field mappings.
  649. drupal_alter('search_api_solr_field_mapping', $index, $ret);
  650. $this->fieldNames[$index->machine_name] = $ret;
  651. }
  652. return $this->fieldNames[$index->machine_name];
  653. }
  654. /**
  655. * Helper method for indexing.
  656. *
  657. * Adds $value with field name $key to the document $doc. The format of $value
  658. * is the same as specified in SearchApiServiceInterface::indexItems().
  659. */
  660. protected function addIndexField(SearchApiSolrDocument $doc, $key, $value, $type, $multi_valued = FALSE) {
  661. $text_content = '';
  662. // Don't index empty values (i.e., when field is missing).
  663. if (!isset($value)) {
  664. return $text_content;
  665. }
  666. if (search_api_is_list_type($type)) {
  667. $type = substr($type, 5, -1);
  668. foreach ($value as $v) {
  669. $text_content .= $this->addIndexField($doc, $key, $v, $type, TRUE) . "\n\n";
  670. }
  671. return trim($text_content);
  672. }
  673. switch ($type) {
  674. case 'tokens':
  675. foreach ($value as $v) {
  676. $text_content .= $v['value'] . ' ';
  677. $doc->addField($key, $v['value']);
  678. }
  679. return trim($text_content);
  680. case 'boolean':
  681. $value = $value ? 'true' : 'false';
  682. break;
  683. case 'date':
  684. $value = is_numeric($value) ? (int) $value : strtotime($value);
  685. if ($value === FALSE) {
  686. return $text_content;
  687. }
  688. $value = format_date($value, 'custom', self::SOLR_DATE_FORMAT, 'UTC');
  689. break;
  690. case 'integer':
  691. $value = (int) $value;
  692. break;
  693. case 'decimal':
  694. $value = (float) $value;
  695. break;
  696. }
  697. if ($multi_valued) {
  698. $doc->addField($key, $value);
  699. }
  700. else {
  701. $doc->setField($key, $value);
  702. }
  703. if (search_api_is_text_type($type)) {
  704. $text_content = $value;
  705. }
  706. return $text_content;
  707. }
  708. /**
  709. * Applies custom modifications to indexed Solr documents.
  710. *
  711. * This method allows subclasses to easily apply custom changes before the
  712. * documents are sent to Solr. The method is empty by default.
  713. *
  714. * @param array $documents
  715. * An array of SearchApiSolrDocument objects ready to be indexed, generated
  716. * from $items array.
  717. * @param SearchApiIndex $index
  718. * The search index for which items are being indexed.
  719. * @param array $items
  720. * An array of items being indexed.
  721. *
  722. * @see hook_search_api_solr_documents_alter()
  723. */
  724. protected function alterSolrDocuments(array &$documents, SearchApiIndex $index, array $items) {
  725. }
  726. /**
  727. * Implements SearchApiServiceInterface::deleteItems().
  728. *
  729. * This method has a custom, Solr-specific extension:
  730. *
  731. * If $ids is a string other than "all", it is treated as a Solr query. All
  732. * items matching that Solr query are then deleted. If $index is additionally
  733. * specified, then only those items also lying on that index will be deleted.
  734. *
  735. * It is up to the caller to ensure $ids is a valid query when the method is
  736. * called in this fashion.
  737. *
  738. * @param array|string $ids
  739. * Either an array containing the ids of the items that should be deleted,
  740. * or 'all' if all items should be deleted. Other formats might be
  741. * recognized by implementing classes, but these are not standardized.
  742. * @param SearchApiIndex $index
  743. * The index from which items should be deleted, or NULL if all indexes on
  744. * this server should be cleared (then, $ids has to be 'all').
  745. *
  746. * @throws SearchApiException
  747. * If an error occurred while trying to delete the items.
  748. */
  749. public function deleteItems($ids = 'all', SearchApiIndex $index = NULL) {
  750. $this->connect();
  751. if (is_array($ids)) {
  752. $index_id = $this->getIndexId($index->machine_name);
  753. $solr_ids = array();
  754. foreach ($ids as $id) {
  755. $solr_ids[] = $this->createId($index_id, $id);
  756. }
  757. $this->solr->deleteByMultipleIds($solr_ids);
  758. }
  759. else {
  760. $query = array();
  761. if ($index) {
  762. $index_id = $this->getIndexId($index->machine_name);
  763. $index_id = call_user_func(array($this->connection_class, 'phrase'), $index_id);
  764. $query[] = "index_id:$index_id";
  765. }
  766. if (!empty($this->options['site_hash'])) {
  767. // We don't need to escape the site hash, as that consists only of
  768. // alphanumeric characters.
  769. $query[] = 'hash:' . search_api_solr_site_hash();
  770. }
  771. if ($ids != 'all') {
  772. $query[] = $query ? "($ids)" : $ids;
  773. }
  774. $this->solr->deleteByQuery($query ? implode(' AND ', $query) : '*:*');
  775. }
  776. $this->scheduleCommit();
  777. }
  778. /**
  779. * {@inheritdoc}
  780. */
  781. public function search(SearchApiQueryInterface $query) {
  782. $time_method_called = microtime(TRUE);
  783. // Reset request handler.
  784. $this->request_handler = NULL;
  785. // Get field information.
  786. $index = $query->getIndex();
  787. $index_id = $this->getIndexId($index->machine_name);
  788. $fields = $this->getFieldNames($index);
  789. // Get Solr connection.
  790. $this->connect();
  791. $version = $this->solr->getSolrVersion();
  792. // Extract keys.
  793. $keys = $query->getKeys();
  794. if (is_array($keys)) {
  795. $keys = $this->flattenKeys($keys);
  796. }
  797. // Set searched fields.
  798. $options = $query->getOptions();
  799. $search_fields = $query->getFields();
  800. // Get the index fields to be able to retrieve boosts.
  801. $index_fields = $index->getFields() + array(
  802. 'search_api_relevance' => array('type' => 'decimal', 'indexed' => TRUE),
  803. 'search_api_id' => array('type' => 'integer', 'indexed' => TRUE),
  804. );
  805. $qf = array();
  806. foreach ($search_fields as $f) {
  807. $boost = isset($index_fields[$f]['boost']) ? '^' . $index_fields[$f]['boost'] : '';
  808. $qf[] = $fields[$f] . $boost;
  809. }
  810. // Extract filters.
  811. $filter = $query->getFilter();
  812. $fq = $this->createFilterQueries($filter, $fields, $index->options['fields']);
  813. $fq[] = 'index_id:' . call_user_func(array($this->connection_class, 'phrase'), $index_id);
  814. if (!empty($this->options['site_hash'])) {
  815. // We don't need to escape the site hash, as that consists only of
  816. // alphanumeric characters.
  817. $fq[] = 'hash:' . search_api_solr_site_hash();
  818. }
  819. // Extract sort.
  820. $sort = array();
  821. foreach ($query->getSort() as $field => $order) {
  822. $f = $fields[$field];
  823. if (substr($f, 0, 3) == 'ss_') {
  824. $f = 'sort_' . substr($f, 3);
  825. }
  826. // The default Solr schema provides a virtual field named "random_SEED"
  827. // that can be used to randomly sort the results; the field is available
  828. // only at query-time.
  829. if ($field == 'search_api_random') {
  830. $params = $query->getOption('search_api_random_sort', array());
  831. // Random seed: getting the value from parameters or computing a new one.
  832. $seed = !empty($params['seed']) ? $params['seed'] : mt_rand();
  833. $f = 'random_' . $seed;
  834. }
  835. $order = strtolower($order);
  836. $sort[$field] = "$f $order";
  837. }
  838. // Get facet fields.
  839. $facets = $query->getOption('search_api_facets', array());
  840. $facet_params = $this->getFacetParams($facets, $fields, $fq);
  841. // Handle highlighting.
  842. $highlight_params = $this->getHighlightParams($query);
  843. // Handle More Like This query.
  844. $mlt = $query->getOption('search_api_mlt');
  845. if ($mlt) {
  846. $mlt_params['qt'] = 'mlt';
  847. // The fields to look for similarities in.
  848. $mlt_fl = array();
  849. // Solr 4 (before 4.6) has a bug which results in numeric fields not being
  850. // supported in MLT queries.
  851. $mlt_no_numeric_fields = FALSE;
  852. if ($version == 4) {
  853. $system_info = $this->solr->getSystemInfo();
  854. $mlt_no_numeric_fields = !isset($system_info->lucene->{'solr-spec-version'}) || version_compare($system_info->lucene->{'solr-spec-version'}, '4.6.0', '<');
  855. }
  856. foreach($mlt['fields'] as $f) {
  857. // Date fields don't seem to be supported at all.
  858. if ($fields[$f][0] === 'd' || ($mlt_no_numeric_fields && in_array($fields[$f][0], array('i', 'f')))) {
  859. continue;
  860. }
  861. $mlt_fl[] = $fields[$f];
  862. // For non-text fields, set minimum word length to 0.
  863. if (isset($index->options['fields'][$f]['type']) && !search_api_is_text_type($index->options['fields'][$f]['type'])) {
  864. $mlt_params['f.' . $fields[$f] . '.mlt.minwl'] = 0;
  865. }
  866. }
  867. $mlt_params['mlt.fl'] = implode(',', $mlt_fl);
  868. $id = $this->createId($index_id, $mlt['id']);
  869. $id = call_user_func(array($this->connection_class, 'phrase'), $id);
  870. $keys = 'id:' . $id;
  871. // In (early versions of) Solr 5, facets aren't supported with MLT.
  872. if ($version >= 5) {
  873. $facet_params = array();
  874. }
  875. }
  876. // Handle spatial filters.
  877. if ($spatials = $query->getOption('search_api_location')) {
  878. foreach ($spatials as $i => $spatial) {
  879. // Spatial options all need a field to do anything.
  880. if (!isset($spatial['field'])) {
  881. continue;
  882. }
  883. unset($radius);
  884. $field = $fields[$spatial['field']];
  885. $escaped_field = call_user_func(array($this->connection_class, 'escapeFieldName'), $field);
  886. // If proper bbox coordinates were given use them to filter.
  887. if (isset($spatial['bbox'])) {
  888. if ($version >= 4) {
  889. $bbox = $spatial['bbox'];
  890. $fq[] = $escaped_field . ':[' . (float) $bbox['bottom'] . ',' . (float) $bbox['left'] . ' TO ' . (float) $bbox['top'] . ',' . (float) $bbox['right'] . ']';
  891. }
  892. else {
  893. $warnings[] = t('Filtering by a bounding box is not supported in Solr versions below 4.');
  894. }
  895. }
  896. // Everything other than a bounding box filter requires a point, so stop
  897. // here (for this option) if "lat" and "lon" aren't both set.
  898. if (!isset($spatial['lat']) || !isset($spatial['lon'])) {
  899. continue;
  900. }
  901. $point = ((float) $spatial['lat']) . ',' . ((float) $spatial['lon']);
  902. // Prepare the filter settings.
  903. if (isset($spatial['radius'])) {
  904. $radius = (float) $spatial['radius'];
  905. }
  906. $spatial_method = 'geofilt';
  907. if (isset($spatial['method']) && in_array($spatial['method'], array('geofilt', 'bbox'))) {
  908. $spatial_method = $spatial['method'];
  909. }
  910. // Change the fq facet ranges to the correct fq.
  911. foreach ($fq as $key => $value) {
  912. // If the fq consists only of a filter on this field, replace it with
  913. // a range.
  914. $preg_field = preg_quote($escaped_field, '/');
  915. if (preg_match('/^(?:\{!tag[^}]+\})?' . $preg_field . ':\["?(\*|\d+(?:\.\d+)?)"? TO "?(\*|\d+(?:\.\d+)?)"?\]$/', $value, $m)) {
  916. unset($fq[$key]);
  917. if ($m[1] && is_numeric($m[1])) {
  918. $min_radius = isset($min_radius) ? max($min_radius, $m[1]) : $m[1];
  919. }
  920. if (is_numeric($m[2])) {
  921. // Make the radius tighter accordingly.
  922. $radius = isset($radius) ? min($radius, $m[2]) : $m[2];
  923. }
  924. }
  925. }
  926. // If either a radius was given in the option, or a filter was
  927. // encountered, set a filter for the lowest value. If a lower boundary
  928. // was set (too), we can only set a filter for that if the field name
  929. // doesn't contains any colons.
  930. if (isset($min_radius) && strpos($field, ':') === FALSE) {
  931. $upper = isset($radius) ? " u=$radius" : '';
  932. $fq[] = "{!frange l=$min_radius$upper}geodist($field,$point)";
  933. }
  934. elseif (isset($radius)) {
  935. $fq[] = "{!$spatial_method pt=$point sfield=$field d=$radius}";
  936. }
  937. // Change sort on the field, if set (and not already changed).
  938. if (isset($sort[$spatial['field']]) && substr($sort[$spatial['field']], 0, strlen($field)) === $field) {
  939. if (strpos($field, ':') === FALSE) {
  940. $sort[$spatial['field']] = str_replace($field, "geodist($field,$point)", $sort[$spatial['field']]);
  941. }
  942. else {
  943. $link = l(t('edit server'), 'admin/config/search/search_api/server/' . $this->server->machine_name . '/edit');
  944. watchdog('search_api_solr', 'Location sort on field @field had to be ignored because unclean field identifiers are used.', array('@field' => $spatial['field']), WATCHDOG_WARNING, $link);
  945. }
  946. }
  947. // Add parameters to fetch distance, if requested.
  948. if (!empty($spatial['distance']) && $version >= 4) {
  949. if (strpos($field, ':') === FALSE) {
  950. // Add pseudofield with the distance to the result items.
  951. $location_fields[] = '_' . $field . '_distance_:geodist(' . $field . ',' . $point . ')';
  952. }
  953. else {
  954. $link = l(t('edit server'), 'admin/config/search/search_api/server/' . $this->server->machine_name . '/edit');
  955. watchdog('search_api_solr', "Location distance information can't be added because unclean field identifiers are used.", array(), WATCHDOG_WARNING, $link);
  956. }
  957. }
  958. // Change the facet parameters for spatial fields to return distance
  959. // facets.
  960. if (!empty($facets)) {
  961. if (!empty($facet_params['facet.field'])) {
  962. $facet_params['facet.field'] = array_diff($facet_params['facet.field'], array($field));
  963. }
  964. foreach ($facets as $delta => $facet) {
  965. if ($facet['field'] != $spatial['field']) {
  966. continue;
  967. }
  968. $steps = $facet['limit'] > 0 ? $facet['limit'] : 5;
  969. $step = (isset($radius) ? $radius : 100) / $steps;
  970. for ($k = $steps - 1; $k > 0; --$k) {
  971. $distance = $step * $k;
  972. $key = "spatial-$delta-$distance";
  973. $facet_params['facet.query'][] = "{!$spatial_method pt=$point sfield=$field d=$distance key=$key}";
  974. }
  975. foreach (array('limit', 'mincount', 'missing') as $setting) {
  976. unset($facet_params["f.$field.facet.$setting"]);
  977. }
  978. }
  979. }
  980. }
  981. }
  982. // Normal sorting on location fields isn't possible.
  983. foreach ($sort as $field => $sort_param) {
  984. if (substr($sort_param, 0, 3) === 'loc') {
  985. unset($sort[$field]);
  986. }
  987. }
  988. // Handle field collapsing / grouping.
  989. $grouping = $query->getOption('search_api_grouping');
  990. if (!empty($grouping['use_grouping'])) {
  991. $group_params['group'] = 'true';
  992. // We always want the number of groups returned so that we get pagers done
  993. // right.
  994. $group_params['group.ngroups'] = 'true';
  995. if (!empty($grouping['truncate'])) {
  996. $group_params['group.truncate'] = 'true';
  997. }
  998. if (!empty($grouping['group_facet'])) {
  999. $group_params['group.facet'] = 'true';
  1000. }
  1001. foreach ($grouping['fields'] as $collapse_field) {
  1002. $type = $index_fields[$collapse_field]['type'];
  1003. // Only single-valued fields are supported.
  1004. if ($version < 4) {
  1005. // For Solr 3.x, only string and boolean fields are supported.
  1006. if (search_api_is_list_type($type) || !search_api_is_text_type($type, array('string', 'boolean', 'uri'))) {
  1007. $warnings[] = t('Grouping is not supported for field @field. ' .
  1008. 'Only single-valued fields of type "String", "Boolean" or "URI" are supported.',
  1009. array('@field' => $index_fields[$collapse_field]['name']));
  1010. continue;
  1011. }
  1012. }
  1013. else {
  1014. if (search_api_is_list_type($type) || search_api_is_text_type($type)) {
  1015. $warnings[] = t('Grouping is not supported for field @field. ' .
  1016. 'Only single-valued fields not indexed as "Fulltext" are supported.',
  1017. array('@field' => $index_fields[$collapse_field]['name']));
  1018. continue;
  1019. }
  1020. }
  1021. $group_params['group.field'][] = $fields[$collapse_field];
  1022. }
  1023. if (empty($group_params['group.field'])) {
  1024. unset($group_params);
  1025. }
  1026. else {
  1027. if (!empty($grouping['group_sort'])) {
  1028. foreach ($grouping['group_sort'] as $group_sort_field => $order) {
  1029. if (isset($fields[$group_sort_field])) {
  1030. $f = $fields[$group_sort_field];
  1031. if (substr($f, 0, 3) == 'ss_') {
  1032. $f = 'sort_' . substr($f, 3);
  1033. }
  1034. // The default Solr schema provides a virtual field named
  1035. // "random_SEED" that can be used to randomly sort the results;
  1036. // the field is available only at query-time.
  1037. if ($group_sort_field == 'search_api_random') {
  1038. $params = $query->getOption('search_api_random_sort', array());
  1039. // Random seed: getting the value from parameters or computing a
  1040. // new one.
  1041. $seed = !empty($params['seed']) ? $params['seed'] : mt_rand();
  1042. $f = 'random_' . $seed;
  1043. }
  1044. $order = strtolower($order);
  1045. $group_params['group.sort'][] = $f . ' ' . $order;
  1046. }
  1047. }
  1048. if (!empty($group_params['group.sort'])) {
  1049. $group_params['group.sort'] = implode(', ', $group_params['group.sort']);
  1050. }
  1051. }
  1052. if (!empty($grouping['group_limit']) && ($grouping['group_limit'] != 1)) {
  1053. $group_params['group.limit'] = $grouping['group_limit'];
  1054. }
  1055. }
  1056. }
  1057. // Set defaults.
  1058. if (!$keys) {
  1059. $keys = NULL;
  1060. }
  1061. // Collect parameters.
  1062. $params = array(
  1063. 'fl' => 'item_id,score',
  1064. 'qf' => $qf,
  1065. 'fq' => $fq,
  1066. );
  1067. if (isset($options['offset'])) {
  1068. $params['start'] = $options['offset'];
  1069. }
  1070. $params['rows'] = isset($options['limit']) ? $options['limit'] : 1000000;
  1071. if ($sort) {
  1072. $params['sort'] = implode(', ', $sort);
  1073. }
  1074. if (!empty($facet_params['facet.field'])) {
  1075. $params += $facet_params;
  1076. }
  1077. if (!empty($highlight_params)) {
  1078. $params += $highlight_params;
  1079. }
  1080. if (!empty($options['search_api_spellcheck'])) {
  1081. $params['spellcheck'] = 'true';
  1082. }
  1083. if (!empty($mlt_params['mlt.fl'])) {
  1084. $params += $mlt_params;
  1085. }
  1086. if (!empty($group_params)) {
  1087. $params += $group_params;
  1088. }
  1089. if (!empty($this->options['retrieve_data'])) {
  1090. $params['fl'] = '*,score';
  1091. }
  1092. if (!empty($location_fields)) {
  1093. $params['fl'] .= ',' . implode(',', $location_fields);
  1094. }
  1095. // Retrieve http method from server options.
  1096. $http_method = !empty($this->options['http_method']) ? $this->options['http_method'] : 'AUTO';
  1097. $call_args = array(
  1098. 'query' => &$keys,
  1099. 'params' => &$params,
  1100. 'http_method' => &$http_method,
  1101. );
  1102. if ($this->request_handler) {
  1103. $this->setRequestHandler($this->request_handler, $call_args);
  1104. }
  1105. try {
  1106. // Send search request.
  1107. $time_processing_done = microtime(TRUE);
  1108. drupal_alter('search_api_solr_query', $call_args, $query);
  1109. $this->preQuery($call_args, $query);
  1110. $response = $this->solr->search($keys, $params, $http_method);
  1111. $time_query_done = microtime(TRUE);
  1112. // Extract results.
  1113. $results = $this->extractResults($query, $response);
  1114. // Add warnings, if present.
  1115. if (!empty($warnings)) {
  1116. $results['warnings'] = isset($results['warnings']) ? array_merge($warnings, $results['warnings']) : $warnings;
  1117. }
  1118. // Extract facets.
  1119. if ($facets = $this->extractFacets($query, $response)) {
  1120. $results['search_api_facets'] = $facets;
  1121. }
  1122. drupal_alter('search_api_solr_search_results', $results, $query, $response);
  1123. $this->postQuery($results, $query, $response);
  1124. // Compute performance.
  1125. $time_end = microtime(TRUE);
  1126. $results['performance'] = array(
  1127. 'complete' => $time_end - $time_method_called,
  1128. 'preprocessing' => $time_processing_done - $time_method_called,
  1129. 'execution' => $time_query_done - $time_processing_done,
  1130. 'postprocessing' => $time_end - $time_query_done,
  1131. );
  1132. return $results;
  1133. }
  1134. catch (SearchApiException $e) {
  1135. throw new SearchApiException(t('An error occurred while trying to search with Solr: @msg.', array('@msg' => $e->getMessage())));
  1136. }
  1137. }
  1138. /**
  1139. * Extract results from a Solr response.
  1140. *
  1141. * @param object $response
  1142. * A HTTP response object.
  1143. *
  1144. * @return array
  1145. * An array with two keys:
  1146. * - result count: The number of total results.
  1147. * - results: An array of search results, as specified by
  1148. * SearchApiQueryInterface::execute().
  1149. */
  1150. protected function extractResults(SearchApiQueryInterface $query, $response) {
  1151. $index = $query->getIndex();
  1152. $fields = $this->getFieldNames($index);
  1153. $field_options = $index->options['fields'];
  1154. $version = $this->solr->getSolrVersion();
  1155. // Set up the results array.
  1156. $results = array();
  1157. $results['results'] = array();
  1158. // Keep a copy of the response in the results so it's possible to extract
  1159. // further useful information out of it, if necessary.
  1160. $results['search_api_solr_response'] = $response;
  1161. // In some rare cases (e.g., MLT query with nonexistent ID) the response
  1162. // will be NULL.
  1163. if (!isset($response->response) && !isset($response->grouped)) {
  1164. $results['result count'] = 0;
  1165. return $results;
  1166. }
  1167. // If field collapsing has been enabled for this query, we need to process
  1168. // the results differently.
  1169. $grouping = $query->getOption('search_api_grouping');
  1170. if (!empty($grouping['use_grouping']) && !empty($response->grouped)) {
  1171. $docs = array();
  1172. $results['result count'] = 0;
  1173. foreach ($grouping['fields'] as $field) {
  1174. if (!empty($response->grouped->{$fields[$field]})) {
  1175. $results['result count'] += $response->grouped->{$fields[$field]}->ngroups;
  1176. foreach ($response->grouped->{$fields[$field]}->groups as $group) {
  1177. foreach ($group->doclist->docs as $doc) {
  1178. $docs[] = $doc;
  1179. }
  1180. }
  1181. }
  1182. }
  1183. }
  1184. else {
  1185. $results['result count'] = $response->response->numFound;
  1186. $docs = $response->response->docs;
  1187. }
  1188. $spatials = $query->getOption('search_api_location');
  1189. // Add each search result to the results array.
  1190. foreach ($docs as $doc) {
  1191. // Blank result array.
  1192. $result = array(
  1193. 'id' => NULL,
  1194. 'score' => NULL,
  1195. 'fields' => array(),
  1196. );
  1197. // Extract properties from the Solr document, translating from Solr to
  1198. // Search API property names. This reverses the mapping in
  1199. // SearchApiSolrService::getFieldNames().
  1200. foreach ($fields as $search_api_property => $solr_property) {
  1201. if (isset($doc->{$solr_property})) {
  1202. $result['fields'][$search_api_property] = $doc->{$solr_property};
  1203. // Date fields need some special treatment to become valid date values
  1204. // (i.e., timestamps) again.
  1205. if (isset($field_options[$search_api_property]['type'])
  1206. && $field_options[$search_api_property]['type'] == 'date'
  1207. && preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/', $result['fields'][$search_api_property])) {
  1208. $result['fields'][$search_api_property] = strtotime($result['fields'][$search_api_property]);
  1209. }
  1210. }
  1211. }
  1212. // We can find the item id and score in the special 'search_api_*'
  1213. // properties. Mappings are provided for these properties in
  1214. // SearchApiSolrService::getFieldNames().
  1215. $result['id'] = $result['fields']['search_api_id'];
  1216. $result['score'] = $result['fields']['search_api_relevance'];
  1217. // If location based search is enabled ensure the calculated distance is
  1218. // set to the appropriate field. If the calculation wasn't possible add
  1219. // the coordinates to allow calculation.
  1220. if ($spatials) {
  1221. foreach ($spatials as $spatial) {
  1222. if (isset($spatial['field']) && !empty($spatial['distance'])) {
  1223. if ($version >= 4) {
  1224. $doc_field = '_' . $fields[$spatial['field']] . '_distance_';
  1225. if (!empty($doc->{$doc_field})) {
  1226. $results['search_api_location'][$spatial['field']][$result['id']]['distance'] = $doc->{$doc_field};
  1227. }
  1228. }
  1229. }
  1230. }
  1231. }
  1232. $index_id = $this->getIndexId($index->machine_name);
  1233. $solr_id = $this->createId($index_id, $result['id']);
  1234. $excerpt = $this->getExcerpt($response, $solr_id, $result['fields'], $fields);
  1235. if ($excerpt) {
  1236. $result['excerpt'] = $excerpt;
  1237. }
  1238. // Use the result's id as the array key. By default, 'id' is mapped to
  1239. // 'item_id' in SearchApiSolrService::getFieldNames().
  1240. if ($result['id']) {
  1241. $results['results'][$result['id']] = $result;
  1242. }
  1243. }
  1244. // Check for spellcheck suggestions.
  1245. if (module_exists('search_api_spellcheck') && $query->getOption('search_api_spellcheck')) {
  1246. $results['search_api_spellcheck'] = new SearchApiSpellcheckSolr($response);
  1247. }
  1248. return $results;
  1249. }
  1250. /**
  1251. * Extract and format highlighting information for a specific item from a Solr response.
  1252. *
  1253. * Will also use highlighted fields to replace retrieved field data, if the
  1254. * corresponding option is set.
  1255. */
  1256. protected function getExcerpt($response, $id, array &$fields, array $field_mapping) {
  1257. if (!isset($response->highlighting->$id)) {
  1258. return FALSE;
  1259. }
  1260. $output = '';
  1261. if (!empty($this->options['excerpt']) && !empty($response->highlighting->$id->content)) {
  1262. foreach ($response->highlighting->$id->content as $snippet) {
  1263. $snippet = strip_tags($snippet);
  1264. $snippet = preg_replace('/^.*>|<.*$/', '', $snippet);
  1265. $snippet = check_plain($snippet);
  1266. $snippet = $this->formatHighlighting($snippet);
  1267. // The created fragments sometimes have leading or trailing punctuation.
  1268. // We remove that here for all common cases, but take care not to remove
  1269. // < or > (so HTML tags stay valid).
  1270. $snippet = trim($snippet, "\00..\x2F:;=\x3F..\x40\x5B..\x60");
  1271. $output .= $snippet . ' … ';
  1272. }
  1273. }
  1274. if (!empty($this->options['highlight_data'])) {
  1275. $prefix = variable_get('search_api_solr_highlight_prefix', 'tm_');
  1276. $prefix_length = strlen($prefix);
  1277. foreach ($field_mapping as $search_api_property => $solr_property) {
  1278. if (substr($solr_property, 0, $prefix_length) == $prefix && !empty($response->highlighting->$id->$solr_property)) {
  1279. $value = $response->highlighting->$id->$solr_property;
  1280. $value = $this->sanitizeHighlightValue($value, $search_api_property);
  1281. // Remove highlight prefixes and suffixes so we could compare values
  1282. // in order to replace the correspond items.
  1283. $orig_value = preg_replace('#\[(/?)HIGHLIGHT\]#', '', $value);
  1284. $field_values = $this->sanitizeHighlightValue($fields[$search_api_property]);
  1285. foreach ($field_values as $delta => $field_value) {
  1286. foreach ($orig_value as $num => $item) {
  1287. if ($item === $field_value) {
  1288. $field_values[$delta] = $this->formatHighlighting($value[$num]);
  1289. $change = TRUE;
  1290. continue 2;
  1291. }
  1292. }
  1293. }
  1294. if (!empty($change)) {
  1295. $fields[$search_api_property] = array(
  1296. '#value' => $field_values,
  1297. '#sanitize_callback' => FALSE,
  1298. );
  1299. }
  1300. }
  1301. }
  1302. }
  1303. return $output;
  1304. }
  1305. /**
  1306. * Sanitizes a highlighted field value.
  1307. *
  1308. * @param string|array $value
  1309. * Either a highlighted field value, or an array of such values.
  1310. * @param string|null $field_id
  1311. * (optional) The ID of the field for which this sanitizing occurs, if any.
  1312. *
  1313. * @return string|array
  1314. * The sanitized input.
  1315. */
  1316. protected function sanitizeHighlightValue($value, $field_id = NULL) {
  1317. if (is_array($value)) {
  1318. foreach ($value as $i => $nested_value) {
  1319. $value[$i] = $this->sanitizeHighlightValue($nested_value, $field_id);
  1320. }
  1321. return $value;
  1322. }
  1323. return check_plain(strip_tags($value));
  1324. }
  1325. /**
  1326. * Changes highlighting tags from our custom, HTML-safe ones to HTML.
  1327. *
  1328. * @param string|string[] $snippet
  1329. * The snippet(s) to format.
  1330. *
  1331. * @return string|string[]
  1332. * The snippet(s), properly formatted as HTML.
  1333. */
  1334. protected function formatHighlighting($snippet) {
  1335. return preg_replace('#\[(/?)HIGHLIGHT\]#', '<$1strong>', $snippet);
  1336. }
  1337. /**
  1338. * Extract facets from a Solr response.
  1339. *
  1340. * @param object $response
  1341. * A response object from SolrPhpClient.
  1342. *
  1343. * @return array
  1344. * An array describing facets that apply to the current results.
  1345. */
  1346. protected function extractFacets(SearchApiQueryInterface $query, $response) {
  1347. $facets = array();
  1348. if (!isset($response->facet_counts)) {
  1349. return $facets;
  1350. }
  1351. $index = $query->getIndex();
  1352. $fields = $this->getFieldNames($index);
  1353. $extract_facets = $query->getOption('search_api_facets', array());
  1354. if (isset($response->facet_counts->facet_fields)) {
  1355. $facet_fields = $response->facet_counts->facet_fields;
  1356. foreach ($extract_facets as $delta => $info) {
  1357. $field = $fields[$info['field']];
  1358. if (!empty($facet_fields->$field)) {
  1359. $min_count = $info['min_count'];
  1360. $terms = $facet_fields->$field;
  1361. if ($info['missing']) {
  1362. // We have to correctly incorporate the "_empty_" term.
  1363. // This will ensure that the term with the least results is dropped,
  1364. // if the limit would be exceeded.
  1365. if (isset($terms->_empty_)) {
  1366. if ($terms->_empty_ < $min_count) {
  1367. unset($terms->_empty_);
  1368. }
  1369. else {
  1370. $terms = (array) $terms;
  1371. arsort($terms);
  1372. if ($info['limit'] > 0 && count($terms) > $info['limit']) {
  1373. array_pop($terms);
  1374. }
  1375. }
  1376. }
  1377. }
  1378. elseif (isset($terms->_empty_)) {
  1379. $terms = clone $terms;
  1380. unset($terms->_empty_);
  1381. }
  1382. $type = isset($index->options['fields'][$info['field']]['type']) ? search_api_extract_inner_type($index->options['fields'][$info['field']]['type']) : 'string';
  1383. foreach ($terms as $term => $count) {
  1384. if ($count >= $min_count) {
  1385. if ($term === '_empty_') {
  1386. $term = '!';
  1387. }
  1388. elseif ($type == 'boolean') {
  1389. if ($term == 'true') {
  1390. $term = '"1"';
  1391. }
  1392. elseif ($term == 'false') {
  1393. $term = '"0"';
  1394. }
  1395. }
  1396. elseif ($type == 'date') {
  1397. $term = $term ? '"' . strtotime($term) . '"' : NULL;
  1398. }
  1399. else {
  1400. $term = "\"$term\"";
  1401. }
  1402. if ($term) {
  1403. $facets[$delta][] = array(
  1404. 'filter' => $term,
  1405. 'count' => $count,
  1406. );
  1407. }
  1408. }
  1409. }
  1410. if (empty($facets[$delta])) {
  1411. unset($facets[$delta]);
  1412. }
  1413. }
  1414. }
  1415. }
  1416. if (isset($response->facet_counts->facet_queries)) {
  1417. if ($spatials = $query->getOption('search_api_location')) {
  1418. $queries = array();
  1419. foreach ($response->facet_counts->facet_queries as $key => $count) {
  1420. if (!preg_match('/^spatial-(.*)-(\d+(?:\.\d+)?)$/', $key, $m)) {
  1421. continue;
  1422. }
  1423. if (empty($extract_facets[$m[1]])) {
  1424. continue;
  1425. }
  1426. $facet = $extract_facets[$m[1]];
  1427. if ($count >= $facet['min_count']) {
  1428. $facets[$m[1]][] = array(
  1429. 'filter' => "[* {$m[2]}]",
  1430. 'count' => $count,
  1431. );
  1432. }
  1433. }
  1434. }
  1435. }
  1436. return $facets;
  1437. }
  1438. /**
  1439. * Flatten a keys array into a single search string.
  1440. *
  1441. * @param array $keys
  1442. * The keys array to flatten, formatted as specified by
  1443. * SearchApiQueryInterface::getKeys().
  1444. *
  1445. * @return string
  1446. * A Solr query string representing the same keys.
  1447. */
  1448. protected function flattenKeys(array $keys) {
  1449. $k = array();
  1450. $or = $keys['#conjunction'] == 'OR';
  1451. $neg = !empty($keys['#negation']);
  1452. foreach (element_children($keys) as $i) {
  1453. $key = $keys[$i];
  1454. if (!$key) {
  1455. continue;
  1456. }
  1457. if (is_array($key)) {
  1458. $subkeys = $this->flattenKeys($key);
  1459. if ($subkeys) {
  1460. $nested_expressions = TRUE;
  1461. // If this is a negated OR expression, we can't just use nested keys
  1462. // as-is, but have to put them into parantheses.
  1463. if ($or && $neg) {
  1464. $subkeys = "($subkeys)";
  1465. }
  1466. $k[] = $subkeys;
  1467. }
  1468. }
  1469. else {
  1470. $key = trim($key);
  1471. $key = call_user_func(array($this->connection_class, 'phrase'), $key);
  1472. $k[] = $key;
  1473. }
  1474. }
  1475. if (!$k) {
  1476. return '';
  1477. }
  1478. // Formatting the keys into a Solr query can be a bit complex. The following
  1479. // code will produce filters that look like this:
  1480. //
  1481. // #conjunction | #negation | return value
  1482. // ----------------------------------------------------------------
  1483. // AND | FALSE | A B C
  1484. // AND | TRUE | -(A AND B AND C)
  1485. // OR | FALSE | ((A) OR (B) OR (C))
  1486. // OR | TRUE | -A -B -C
  1487. // If there was just a single, unnested key, we can ignore all this.
  1488. if (count($k) == 1 && empty($nested_expressions)) {
  1489. $k = reset($k);
  1490. return $neg ? "*:* AND -$k" : $k;
  1491. }
  1492. if ($or) {
  1493. if ($neg) {
  1494. return '*:* AND -' . implode(' AND -', $k);
  1495. }
  1496. return '((' . implode(') OR (', $k) . '))';
  1497. }
  1498. $k = implode(' AND ', $k);
  1499. return $neg ? "*:* AND -($k)" : $k;
  1500. }
  1501. /**
  1502. * Transforms a query filter into a flat array of Solr filter queries, using
  1503. * the field names in $fields.
  1504. */
  1505. protected function createFilterQueries(SearchApiQueryFilterInterface $filter, array $solr_fields, array $fields) {
  1506. $or = $filter->getConjunction() == 'OR';
  1507. $fq = array();
  1508. $prefix = '';
  1509. foreach ($filter->getFilters() as $f) {
  1510. if (is_array($f)) {
  1511. if (!isset($fields[$f[0]])) {
  1512. throw new SearchApiException(t('Filter term on unknown or unindexed field @field.', array('@field' => $f[0])));
  1513. }
  1514. if ($f[1] !== '') {
  1515. $fq[] = $this->createFilterQuery($solr_fields[$f[0]], $f[1], $f[2], $fields[$f[0]]);
  1516. }
  1517. }
  1518. elseif ($f instanceof SearchApiQueryFilterInterface) {
  1519. $q = $this->createFilterQueries($f, $solr_fields, $fields);
  1520. if ($filter->getConjunction() != $f->getConjunction() && count($q) > 1) {
  1521. $fq[] = '((' . implode(') ' . $f->getConjunction() . ' (', $q) . '))';
  1522. }
  1523. else {
  1524. $fq = array_merge($fq, $q);
  1525. }
  1526. }
  1527. }
  1528. if (method_exists($filter, 'getTags')) {
  1529. foreach ($filter->getTags() as $tag) {
  1530. $prefix = "{!tag=$tag}";
  1531. // We can only apply one tag per filter.
  1532. break;
  1533. }
  1534. }
  1535. if ($or && count($fq) > 1) {
  1536. $fq = array('((' . implode(') OR (', $fq) . '))');
  1537. }
  1538. if ($prefix) {
  1539. foreach ($fq as $i => $filter) {
  1540. $fq[$i] = $prefix . $filter;
  1541. }
  1542. }
  1543. return $fq;
  1544. }
  1545. /**
  1546. * Create a single search query string according to the given field, value
  1547. * and operator.
  1548. */
  1549. protected function createFilterQuery($field, $value, $operator, $field_info) {
  1550. $field = call_user_func(array($this->connection_class, 'escapeFieldName'), $field);
  1551. // Special handling for location fields.
  1552. if (isset($field_info['real_type']) && $field_info['real_type'] == 'location') {
  1553. // Empty / non-empty comparison has to take place in one of the subfields
  1554. // of the location field type. These subfields are usually generated with
  1555. // the index and the field type as name suffix.
  1556. // @TODO Do we need to handle other operators / values too?
  1557. if ($value === NULL) {
  1558. $field .= '_0___tdouble';
  1559. }
  1560. }
  1561. if ($value === NULL) {
  1562. return ($operator == '=' ? '*:* AND -' : '') . "$field:[* TO *]";
  1563. }
  1564. $value = $this->formatFilterValue($value, search_api_extract_inner_type($field_info['type']));
  1565. switch ($operator) {
  1566. case '<>':
  1567. return "*:* AND -($field:$value)";
  1568. case '<':
  1569. return "$field:{* TO $value}";
  1570. case '<=':
  1571. return "$field:[* TO $value]";
  1572. case '>=':
  1573. return "$field:[$value TO *]";
  1574. case '>':
  1575. return "$field:{{$value} TO *}";
  1576. default:
  1577. return "$field:$value";
  1578. }
  1579. }
  1580. /**
  1581. * Format a value for filtering on a field of a specific type.
  1582. */
  1583. protected function formatFilterValue($value, $type) {
  1584. switch ($type) {
  1585. case 'boolean':
  1586. $value = $value ? 'true' : 'false';
  1587. break;
  1588. case 'date':
  1589. $value = is_numeric($value) ? (int) $value : strtotime($value);
  1590. if ($value === FALSE) {
  1591. return 0;
  1592. }
  1593. $value = format_date($value, 'custom', self::SOLR_DATE_FORMAT, 'UTC');
  1594. break;
  1595. case 'text':
  1596. return '(' . call_user_func(array($this->connection_class, 'escape'), $value) . ')';
  1597. }
  1598. return call_user_func(array($this->connection_class, 'phrase'), $value);
  1599. }
  1600. /**
  1601. * Helper method for creating the facet field parameters.
  1602. */
  1603. protected function getFacetParams(array $facets, array $fields, array &$fq = array()) {
  1604. if (!$facets) {
  1605. return array();
  1606. }
  1607. $facet_params['facet'] = 'true';
  1608. $facet_params['facet.sort'] = 'count';
  1609. $facet_params['facet.limit'] = 10;
  1610. $facet_params['facet.mincount'] = 1;
  1611. $facet_params['facet.missing'] = 'false';
  1612. foreach ($facets as $info) {
  1613. if (empty($fields[$info['field']])) {
  1614. continue;
  1615. }
  1616. // String fields have their own corresponding facet fields.
  1617. $field = $fields[$info['field']];
  1618. // Check for the "or" operator.
  1619. if (isset($info['operator']) && $info['operator'] === 'or') {
  1620. // Remember that filters for this field should be tagged.
  1621. $tag = 'facet:' . $info['field'];
  1622. // Add the facet field.
  1623. $facet_params['facet.field'][] = "{!ex=$tag}$field";
  1624. }
  1625. else {
  1626. // Add the facet field.
  1627. $facet_params['facet.field'][] = $field;
  1628. }
  1629. // Set limit, unless it's the default.
  1630. if ($info['limit'] != 10) {
  1631. $facet_params["f.$field.facet.limit"] = $info['limit'] ? $info['limit'] : -1;
  1632. }
  1633. // Set mincount, unless it's the default.
  1634. if ($info['min_count'] != 1) {
  1635. $facet_params["f.$field.facet.mincount"] = $info['min_count'];
  1636. }
  1637. // Set missing, if specified.
  1638. if ($info['missing']) {
  1639. $facet_params["f.$field.facet.missing"] = 'true';
  1640. }
  1641. }
  1642. return $facet_params;
  1643. }
  1644. /**
  1645. * Helper method for creating the highlighting parameters.
  1646. *
  1647. * (The $query parameter currently isn't used and only here for the potential
  1648. * sake of subclasses.)
  1649. *
  1650. * @param SearchApiQueryInterface|SearchApiMultiQueryInterface $query
  1651. * The query object, either for a normal Search API query or a multi-index
  1652. * query.
  1653. *
  1654. * @return array
  1655. * An array of parameters to be added to the Solr search request.
  1656. */
  1657. protected function getHighlightParams($query) {
  1658. $highlight_params = array();
  1659. if (!empty($this->options['excerpt']) || !empty($this->options['highlight_data'])) {
  1660. $highlight_params['hl'] = 'true';
  1661. $highlight_params['hl.fl'] = 'content';
  1662. $highlight_params['hl.simple.pre'] = '[HIGHLIGHT]';
  1663. $highlight_params['hl.simple.post'] = '[/HIGHLIGHT]';
  1664. $highlight_params['hl.snippets'] = 3;
  1665. $highlight_params['hl.fragsize'] = 70;
  1666. $highlight_params['hl.mergeContiguous'] = 'true';
  1667. }
  1668. if (!empty($this->options['highlight_data'])) {
  1669. $highlight_params['hl.fl'] = variable_get('search_api_solr_highlight_prefix', 'tm_') . '*';
  1670. $highlight_params['hl.snippets'] = 1;
  1671. $highlight_params['hl.fragsize'] = 0;
  1672. if (!empty($this->options['excerpt'])) {
  1673. // If we also generate a "normal" excerpt, set the settings for the
  1674. // "content" field (which we use to generate the excerpt) back to the
  1675. // above values.
  1676. $highlight_params['f.content.hl.snippets'] = 3;
  1677. $highlight_params['f.content.hl.fragsize'] = 70;
  1678. // It regrettably doesn't seem to be possible to set hl.fl to several
  1679. // values, if one contains wild cards (i.e., "t_*,content" wouldn't
  1680. // work).
  1681. $highlight_params['hl.fl'] = '*';
  1682. }
  1683. }
  1684. return $highlight_params;
  1685. }
  1686. /**
  1687. * Sets the request handler.
  1688. *
  1689. * This should also make the needed adjustments to the request parameters.
  1690. *
  1691. * @param $handler
  1692. * Name of the handler to set.
  1693. * @param array $call_args
  1694. * An associative array containing all three arguments to the
  1695. * SearchApiSolrConnectionInterface::search() call ("query", "params" and
  1696. * "method") as references.
  1697. *
  1698. * @return bool
  1699. * TRUE iff this method invocation handled the given handler. This allows
  1700. * subclasses to recognize whether the request handler was already set by
  1701. * this method.
  1702. */
  1703. protected function setRequestHandler($handler, array &$call_args) {
  1704. if ($handler == 'pinkPony') {
  1705. $call_args['params']['qt'] = $handler;
  1706. return TRUE;
  1707. }
  1708. return FALSE;
  1709. }
  1710. /**
  1711. * Empty method called before sending a search query to Solr.
  1712. *
  1713. * This allows subclasses to apply custom changes before the query is sent to
  1714. * Solr. Works exactly like hook_search_api_solr_query_alter().
  1715. *
  1716. * @param array $call_args
  1717. * An associative array containing all three arguments to the
  1718. * SearchApiSolrConnectionInterface::search() call ("query", "params" and
  1719. * "method") as references.
  1720. * @param SearchApiQueryInterface $query
  1721. * The SearchApiQueryInterface object representing the executed search query.
  1722. */
  1723. protected function preQuery(array &$call_args, SearchApiQueryInterface $query) {
  1724. }
  1725. /**
  1726. * Empty method to allow subclasses to apply custom changes before search results are returned.
  1727. *
  1728. * Works exactly like hook_search_api_solr_search_results_alter().
  1729. *
  1730. * @param array $results
  1731. * The results array that will be returned for the search.
  1732. * @param SearchApiQueryInterface $query
  1733. * The SearchApiQueryInterface object representing the executed search query.
  1734. * @param object $response
  1735. * The response object returned by Solr.
  1736. */
  1737. protected function postQuery(array &$results, SearchApiQueryInterface $query, $response) {
  1738. }
  1739. //
  1740. // Autocompletion feature
  1741. //
  1742. /**
  1743. * Implements SearchApiAutocompleteInterface::getAutocompleteSuggestions().
  1744. */
  1745. // Largely copied from the apachesolr_autocomplete module.
  1746. public function getAutocompleteSuggestions(SearchApiQueryInterface $query, SearchApiAutocompleteSearch $search, $incomplete_key, $user_input) {
  1747. $suggestions = array();
  1748. // Reset request handler
  1749. $this->request_handler = NULL;
  1750. // Turn inputs to lower case, otherwise we get case sensivity problems.
  1751. $incomp = drupal_strtolower($incomplete_key);
  1752. $index = $query->getIndex();
  1753. $fields = $this->getFieldNames($index);
  1754. $complete = $query->getOriginalKeys();
  1755. // Extract keys
  1756. $keys = $query->getKeys();
  1757. if (is_array($keys)) {
  1758. $keys_array = array();
  1759. while ($keys) {
  1760. reset($keys);
  1761. if (!element_child(key($keys))) {
  1762. array_shift($keys);
  1763. continue;
  1764. }
  1765. $key = array_shift($keys);
  1766. if (is_array($key)) {
  1767. $keys = array_merge($keys, $key);
  1768. }
  1769. else {
  1770. $keys_array[$key] = $key;
  1771. }
  1772. }
  1773. $keys = $this->flattenKeys($query->getKeys());
  1774. }
  1775. else {
  1776. $keys_array = drupal_map_assoc(preg_split('/[-\s():{}\[\]\\\\"]+/', $keys, -1, PREG_SPLIT_NO_EMPTY));
  1777. }
  1778. if (!$keys) {
  1779. $keys = NULL;
  1780. }
  1781. // Set searched fields
  1782. $options = $query->getOptions();
  1783. $search_fields = $query->getFields();
  1784. $qf = array();
  1785. foreach ($search_fields as $f) {
  1786. $qf[] = $fields[$f];
  1787. }
  1788. // Extract filters
  1789. $fq = $this->createFilterQueries($query->getFilter(), $fields, $index->options['fields']);
  1790. $index_id = $this->getIndexId($index->machine_name);
  1791. $fq[] = 'index_id:' . call_user_func(array($this->connection_class, 'phrase'), $index_id);
  1792. if (!empty($this->options['site_hash'])) {
  1793. // We don't need to escape the site hash, as that consists only of
  1794. // alphanumeric characters.
  1795. $fq[] = 'hash:' . search_api_solr_site_hash();
  1796. }
  1797. // Autocomplete magic
  1798. $facet_fields = array();
  1799. foreach ($search_fields as $f) {
  1800. $facet_fields[] = $fields[$f];
  1801. }
  1802. $limit = $query->getOption('limit', 10);
  1803. $params = array(
  1804. 'qf' => $qf,
  1805. 'fq' => $fq,
  1806. 'rows' => 0,
  1807. 'facet' => 'true',
  1808. 'facet.field' => $facet_fields,
  1809. 'facet.prefix' => $incomp,
  1810. 'facet.limit' => $limit * 5,
  1811. 'facet.mincount' => 1,
  1812. 'spellcheck' => (!isset($this->options['autocorrect_spell']) || $this->options['autocorrect_spell']) ? 'true' : 'false',
  1813. 'spellcheck.count' => 1,
  1814. );
  1815. // Retrieve http method from server options.
  1816. $http_method = !empty($this->options['http_method']) ? $this->options['http_method'] : 'AUTO';
  1817. $call_args = array(
  1818. 'query' => &$keys,
  1819. 'params' => &$params,
  1820. 'http_method' => &$http_method,
  1821. );
  1822. if ($this->request_handler) {
  1823. $this->setRequestHandler($this->request_handler, $call_args);
  1824. }
  1825. $second_pass = !isset($this->options['autocorrect_suggest_words']) || $this->options['autocorrect_suggest_words'];
  1826. $alter_data = array(
  1827. 'search' => $search,
  1828. 'query' => $query,
  1829. 'incomplete_key' => $incomplete_key,
  1830. 'user_input' => $user_input,
  1831. );
  1832. for ($i = 0; $i < ($second_pass ? 2 : 1); ++$i) {
  1833. try {
  1834. // Send search request
  1835. $this->connect();
  1836. drupal_alter('search_api_solr_query', $call_args, $query);
  1837. $this->preQuery($call_args, $query);
  1838. $response = $this->solr->search($keys, $params, $http_method);
  1839. $alter_data['responses'][] = $response;
  1840. if (!empty($response->spellcheck->suggestions)) {
  1841. $replace = array();
  1842. foreach ($response->spellcheck->suggestions as $word => $data) {
  1843. $replace[$word] = $data->suggestion[0];
  1844. }
  1845. $corrected = str_ireplace(array_keys($replace), array_values($replace), $user_input);
  1846. if ($corrected != $user_input) {
  1847. array_unshift($suggestions, array(
  1848. 'prefix' => t('Did you mean') . ':',
  1849. 'user_input' => $corrected,
  1850. ));
  1851. }
  1852. }
  1853. $matches = array();
  1854. if (isset($response->facet_counts->facet_fields)) {
  1855. foreach ($response->facet_counts->facet_fields as $terms) {
  1856. foreach ($terms as $term => $count) {
  1857. if (isset($matches[$term])) {
  1858. // If we just add the result counts, we can easily get over the
  1859. // total number of results if terms appear in multiple fields.
  1860. // Therefore, we just take the highest value from any field.
  1861. $matches[$term] = max($matches[$term], $count);
  1862. }
  1863. else {
  1864. $matches[$term] = $count;
  1865. }
  1866. }
  1867. }
  1868. if ($matches) {
  1869. // Eliminate suggestions that are too short or already in the query.
  1870. foreach ($matches as $term => $count) {
  1871. if (strlen($term) < 3 || isset($keys_array[$term])) {
  1872. unset($matches[$term]);
  1873. }
  1874. }
  1875. // Don't suggest terms that are too frequent (by default in more
  1876. // than 90% of results).
  1877. $result_count = $response->response->numFound;
  1878. $max_occurrences = $result_count * variable_get('search_api_solr_autocomplete_max_occurrences', 0.9);
  1879. if (($max_occurrences >= 1 || $i > 0) && $max_occurrences < $result_count) {
  1880. foreach ($matches as $match => $count) {
  1881. if ($count > $max_occurrences) {
  1882. unset($matches[$match]);
  1883. }
  1884. }
  1885. }
  1886. // The $count in this array is actually a score. We want the
  1887. // highest ones first.
  1888. arsort($matches);
  1889. // Shorten the array to the right ones.
  1890. $additional_matches = array_slice($matches, $limit - count($suggestions), NULL, TRUE);
  1891. $matches = array_slice($matches, 0, $limit, TRUE);
  1892. // Build suggestions using returned facets
  1893. $incomp_length = strlen($incomp);
  1894. foreach ($matches as $term => $count) {
  1895. if (drupal_strtolower(substr($term, 0, $incomp_length)) == $incomp) {
  1896. $suggestions[] = array(
  1897. 'suggestion_suffix' => substr($term, $incomp_length),
  1898. 'term' => $term,
  1899. 'results' => $count,
  1900. );
  1901. }
  1902. else {
  1903. $suggestions[] = array(
  1904. 'suggestion_suffix' => ' ' . $term,
  1905. 'term' => $term,
  1906. 'results' => $count,
  1907. );
  1908. }
  1909. }
  1910. }
  1911. }
  1912. }
  1913. catch (SearchApiException $e) {
  1914. watchdog_exception('search_api_solr', $e, "%type during autocomplete Solr query: !message in %function (line %line of %file).", array(), WATCHDOG_WARNING);
  1915. }
  1916. if (count($suggestions) >= $limit) {
  1917. break;
  1918. }
  1919. // Change parameters for second query.
  1920. unset($params['facet.prefix']);
  1921. $keys = trim($keys . ' ' . $incomplete_key);
  1922. }
  1923. drupal_alter('search_api_solr_autocomplete_suggestions', $suggestions, $alter_data);
  1924. return $suggestions;
  1925. }
  1926. //
  1927. // SearchApiMultiServiceInterface methods
  1928. //
  1929. /**
  1930. * Implements SearchApiMultiServiceInterface::queryMultiple().
  1931. */
  1932. public function queryMultiple(array $options = array()) {
  1933. return new SearchApiMultiQuery($this->server, $options);
  1934. }
  1935. /**
  1936. * Implements SearchApiMultiServiceInterface::searchMultiple().
  1937. */
  1938. public function searchMultiple(SearchApiMultiQueryInterface $query) {
  1939. $time_method_called = microtime(TRUE);
  1940. // Get field information
  1941. $solr_fields = array(
  1942. 'search_api_id' => 'item_id',
  1943. 'search_api_relevance' => 'score',
  1944. 'search_api_multi_index' => 'index_id',
  1945. );
  1946. $fields = array(
  1947. 'search_api_multi_index' => array(
  1948. 'type' => 'string',
  1949. ),
  1950. );
  1951. foreach ($query->getIndexes() as $index) {
  1952. if (empty($index->options['fields'])) {
  1953. continue;
  1954. }
  1955. $prefix = $this->getIndexId($index->machine_name) . ':';
  1956. foreach ($this->getFieldNames($index) as $field => $key) {
  1957. if (!isset($solr_fields[$field])) {
  1958. $solr_fields[$prefix . $field] = $key;
  1959. }
  1960. }
  1961. foreach ($index->options['fields'] as $field => $info) {
  1962. $fields[$prefix . $field] = $info;
  1963. }
  1964. }
  1965. // Extract keys
  1966. $keys = $query->getKeys();
  1967. if (is_array($keys)) {
  1968. $keys = $this->flattenKeys($keys);
  1969. }
  1970. // Set searched fields
  1971. $search_fields = $query->getFields();
  1972. $qf = array();
  1973. foreach ($search_fields as $f) {
  1974. $boost = isset($fields[$f]['boost']) ? '^' . $fields[$f]['boost'] : '';
  1975. $qf[] = $solr_fields[$f] . $boost;
  1976. }
  1977. // Extract filters
  1978. $filter = $query->getFilter();
  1979. $fq = $this->createFilterQueries($filter, $solr_fields, $fields);
  1980. // Restrict search to searched indexes.
  1981. $index_filter = array();
  1982. $indexes = array();
  1983. foreach ($query->getIndexes() as $index) {
  1984. $index_id = $this->getIndexId($index->machine_name);
  1985. $indexes[$index_id] = $index;
  1986. $index_filter[] = 'index_id:' . call_user_func(array($this->connection_class, 'phrase'), $index_id);
  1987. }
  1988. $fq[] = implode(' OR ', $index_filter);
  1989. if (!empty($this->options['site_hash'])) {
  1990. // We don't need to escape the site hash, as that consists only of
  1991. // alphanumeric characters.
  1992. $fq[] = 'hash:' . search_api_solr_site_hash();
  1993. }
  1994. // Extract sort
  1995. $sort = array();
  1996. foreach ($query->getSort() as $f => $order) {
  1997. $f = $solr_fields[$f];
  1998. if (substr($f, 0, 3) == 'ss_') {
  1999. $f = 'sort_' . substr($f, 3);
  2000. }
  2001. $order = strtolower($order);
  2002. $sort[] = "$f $order";
  2003. }
  2004. // Get facet fields
  2005. $facets = $query->getOption('search_api_facets') ? $query->getOption('search_api_facets') : array();
  2006. $facet_params = $this->getFacetParams($facets, $solr_fields, $fq);
  2007. // Handle highlighting.
  2008. $highlight_params = $this->getHighlightParams($query);
  2009. // Set defaults
  2010. if (!$keys) {
  2011. $keys = NULL;
  2012. }
  2013. $options = $query->getOptions();
  2014. // Collect parameters
  2015. $params = array(
  2016. 'fl' => 'item_id,index_id,score',
  2017. 'qf' => $qf,
  2018. 'fq' => $fq,
  2019. );
  2020. if (isset($options['offset'])) {
  2021. $params['start'] = $options['offset'];
  2022. }
  2023. if (isset($options['limit'])) {
  2024. $params['rows'] = $options['limit'];
  2025. }
  2026. if ($sort) {
  2027. $params['sort'] = implode(', ', $sort);
  2028. }
  2029. if (!empty($facet_params['facet.field'])) {
  2030. $params += $facet_params;
  2031. }
  2032. if (!empty($highlight_params)) {
  2033. $params += $highlight_params;
  2034. }
  2035. if (!empty($this->options['retrieve_data'])) {
  2036. $params['fl'] = '*,score';
  2037. }
  2038. // Retrieve http method from server options.
  2039. $http_method = !empty($this->options['http_method']) ? $this->options['http_method'] : 'AUTO';
  2040. // Send search request
  2041. $time_processing_done = microtime(TRUE);
  2042. $this->connect();
  2043. $call_args = array(
  2044. 'query' => &$keys,
  2045. 'params' => &$params,
  2046. 'http_method' => &$http_method,
  2047. );
  2048. drupal_alter('search_api_solr_multi_query', $call_args, $query);
  2049. $response = $this->solr->search($keys, $params, $http_method);
  2050. $time_query_done = microtime(TRUE);
  2051. // Extract results
  2052. $results = array();
  2053. $results['result count'] = $response->response->numFound;
  2054. $results['results'] = array();
  2055. $tmp = array();
  2056. foreach ($response->response->docs as $id => $doc) {
  2057. if (isset($indexes[$doc->index_id])) {
  2058. $index = $indexes[$doc->index_id];
  2059. }
  2060. else {
  2061. $index = new SearchApiIndex(array('machine_name' => $doc->index_id));
  2062. }
  2063. $fields = $this->getFieldNames($index);
  2064. $field_options = $index->options['fields'];
  2065. $result = array(
  2066. 'id' => NULL,
  2067. 'index_id' => $doc->index_id,
  2068. 'score' => NULL,
  2069. 'fields' => array(),
  2070. );
  2071. $solr_id = $this->createId($doc->index_id, $doc->item_id);
  2072. foreach ($fields as $search_api_property => $solr_property) {
  2073. if (isset($doc->{$solr_property})) {
  2074. $result['fields'][$search_api_property] = $doc->{$solr_property};
  2075. // Date fields need some special treatment to become valid date values
  2076. // (i.e., timestamps) again.
  2077. if (isset($field_options[$search_api_property]['type'])
  2078. && $field_options[$search_api_property]['type'] == 'date'
  2079. && preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/', $result['fields'][$search_api_property])) {
  2080. $result['fields'][$search_api_property] = strtotime($result['fields'][$search_api_property]);
  2081. }
  2082. }
  2083. }
  2084. $excerpt = $this->getExcerpt($response, $solr_id, $result['fields'], $fields);
  2085. if ($excerpt) {
  2086. $result['excerpt'] = $excerpt;
  2087. }
  2088. // We can find the item id and score in the special 'search_api_*'
  2089. // properties. Mappings are provided for these properties in
  2090. // SearchApiSolrService::getFieldNames().
  2091. $result['id'] = $result['fields']['search_api_id'];
  2092. $result['score'] = $result['fields']['search_api_relevance'];
  2093. $results['results'][$id] = $result;
  2094. }
  2095. // Extract facets
  2096. if (isset($response->facet_counts->facet_fields)) {
  2097. $results['search_api_facets'] = array();
  2098. $facet_fields = $response->facet_counts->facet_fields;
  2099. foreach ($facets as $delta => $info) {
  2100. $field = $solr_fields[$info['field']];
  2101. if (!empty($facet_fields->$field)) {
  2102. $min_count = $info['min_count'];
  2103. $terms = $facet_fields->$field;
  2104. if ($info['missing']) {
  2105. // We have to correctly incorporate the "_empty_" term.
  2106. // This will ensure that the term with the least results is dropped,
  2107. // if the limit would be exceeded.
  2108. if (isset($terms->_empty_)) {
  2109. if ($terms->_empty_ < $min_count) {
  2110. unset($terms->_empty_);
  2111. }
  2112. else {
  2113. $terms = (array) $terms;
  2114. arsort($terms);
  2115. if ($info['limit'] > 0 && count($terms) > $info['limit']) {
  2116. array_pop($terms);
  2117. }
  2118. }
  2119. }
  2120. }
  2121. elseif (isset($terms->_empty_)) {
  2122. $terms = clone $terms;
  2123. unset($terms->_empty_);
  2124. }
  2125. $type = isset($fields[$info['field']]['type']) ? search_api_extract_inner_type($fields[$info['field']]['type']) : 'string';
  2126. foreach ($terms as $term => $count) {
  2127. if ($count >= $min_count) {
  2128. if ($term === '_empty_') {
  2129. $term = '!';
  2130. }
  2131. elseif ($type == 'boolean') {
  2132. if ($term == 'true') {
  2133. $term = '"1"';
  2134. }
  2135. elseif ($term == 'false') {
  2136. $term = '"0"';
  2137. }
  2138. }
  2139. elseif ($type == 'date') {
  2140. $term = $term ? '"' . strtotime($term) . '"' : NULL;
  2141. }
  2142. else {
  2143. $term = "\"$term\"";
  2144. }
  2145. if ($term) {
  2146. $results['search_api_facets'][$delta][] = array(
  2147. 'filter' => $term,
  2148. 'count' => $count,
  2149. );
  2150. }
  2151. }
  2152. }
  2153. if (empty($results['search_api_facets'][$delta])) {
  2154. unset($results['search_api_facets'][$delta]);
  2155. }
  2156. }
  2157. }
  2158. }
  2159. drupal_alter('search_api_solr_multi_search_results', $results, $query, $response);
  2160. // Compute performance
  2161. $time_end = microtime(TRUE);
  2162. $results['performance'] = array(
  2163. 'complete' => $time_end - $time_method_called,
  2164. 'preprocessing' => $time_processing_done - $time_method_called,
  2165. 'execution' => $time_query_done - $time_processing_done,
  2166. 'postprocessing' => $time_end - $time_query_done,
  2167. );
  2168. return $results;
  2169. }
  2170. //
  2171. // Additional methods that might be used when knowing the service class.
  2172. //
  2173. /**
  2174. * Ping the Solr server to tell whether it can be accessed.
  2175. *
  2176. * Uses the admin/ping request handler.
  2177. */
  2178. public function ping() {
  2179. $this->connect();
  2180. return $this->solr->ping();
  2181. }
  2182. /**
  2183. * Sends a commit command to the Solr server.
  2184. */
  2185. public function commit() {
  2186. // If committing has been disabled altogether, do nothing here.
  2187. if (!empty($this->options['commits_disabled'])) {
  2188. return;
  2189. }
  2190. try {
  2191. $this->connect();
  2192. return $this->solr->commit(FALSE);
  2193. }
  2194. catch (SearchApiException $e) {
  2195. watchdog_exception('search_api_solr', $e,
  2196. '%type while trying to commit on server @server: !message in %function (line %line of %file).',
  2197. array('@server' => $this->server->machine_name), WATCHDOG_WARNING);
  2198. }
  2199. }
  2200. /**
  2201. * Schedules a commit operation for this server.
  2202. *
  2203. * The commit will be sent at the end of the current page request. Multiple
  2204. * calls to this method will still only result in one commit operation.
  2205. */
  2206. public function scheduleCommit() {
  2207. if (!$this->commitScheduled) {
  2208. $this->commitScheduled = TRUE;
  2209. drupal_register_shutdown_function(array($this, 'commit'));
  2210. }
  2211. }
  2212. /**
  2213. * Gets the Solr connection class used by this service.
  2214. *
  2215. * @return string
  2216. * The name of a class which implements SearchApiSolrConnectionInterface.
  2217. */
  2218. public function getConnectionClass() {
  2219. return $this->connection_class;
  2220. }
  2221. /**
  2222. * Sets the Solr connection class used by this service.
  2223. *
  2224. * @param string $class
  2225. * The name of a class which implements SearchApiSolrConnectionInterface.
  2226. */
  2227. public function setConnectionClass($class) {
  2228. $this->connection_class = $class;
  2229. $this->solr = NULL;
  2230. }
  2231. /**
  2232. * Gets the currently used Solr connection object.
  2233. *
  2234. * @return SearchApiSolrConnectionInterface
  2235. * The solr connection object used by this server.
  2236. */
  2237. public function getSolrConnection() {
  2238. $this->connect();
  2239. return $this->solr;
  2240. }
  2241. /**
  2242. * Get metadata about fields in the Solr/Lucene index.
  2243. *
  2244. * @param int $num_terms
  2245. * Number of 'top terms' to return.
  2246. *
  2247. * @return array
  2248. * An array of SearchApiSolrField objects.
  2249. *
  2250. * @see SearchApiSolrConnectionInterface::getFields()
  2251. */
  2252. public function getFields($num_terms = 0) {
  2253. $this->connect();
  2254. return $this->solr->getFields($num_terms);
  2255. }
  2256. /**
  2257. * Retrieves a config file or file list from the Solr server.
  2258. *
  2259. * Uses the admin/file request handler.
  2260. *
  2261. * @param string|null $file
  2262. * (optional) The name of the file to retrieve. If the file is a directory,
  2263. * the directory contents are instead listed and returned. NULL represents
  2264. * the root config directory.
  2265. *
  2266. * @return object
  2267. * A HTTP response object containing either the file contents or a file list.
  2268. */
  2269. public function getFile($file = NULL) {
  2270. $this->connect();
  2271. $file_servlet_name = constant($this->connection_class . '::FILE_SERVLET');
  2272. $params['contentType'] = 'text/xml;charset=utf-8';
  2273. if ($file) {
  2274. $params['file'] = $file;
  2275. }
  2276. return $this->solr->makeServletRequest($file_servlet_name, $params);
  2277. }
  2278. /**
  2279. * Prefixes an index ID as configured.
  2280. *
  2281. * The resulting ID will be a concatenation of the following strings:
  2282. * - If set, the "search_api_solr_index_prefix" variable.
  2283. * - If set, the index-specific "search_api_solr_index_prefix_INDEX" variable.
  2284. * - The index's machine name.
  2285. *
  2286. * @param string $machine_name
  2287. * The index's machine name.
  2288. *
  2289. * @return string
  2290. * The prefixed machine name.
  2291. */
  2292. protected function getIndexId($machine_name) {
  2293. // Prepend per-index prefix.
  2294. $id = variable_get('search_api_solr_index_prefix_' . $machine_name, '') . $machine_name;
  2295. // Prepend environment prefix.
  2296. $id = variable_get('search_api_solr_index_prefix', '') . $id;
  2297. return $id;
  2298. }
  2299. }