PageRenderTime 60ms CodeModel.GetById 30ms RepoModel.GetById 0ms app.codeStats 0ms

/drupal/sites/all/modules/apachesolr/temp/Solr_Base_Query.php

https://github.com/kmewhort/nomus
PHP | 477 lines | 279 code | 47 blank | 151 comment | 48 complexity | d35f7794b5c0ce3d7d8c23e740873c95 MD5 | raw file
  1. <?php
  2. // $Id: Solr_Base_Query.php,v 1.1.4.40.2.12 2010/03/22 17:38:07 robertDouglass Exp $
  3. class Solr_Base_Query implements Drupal_Solr_Query_Interface {
  4. /**
  5. * Extract all uses of one named field from a filter string e.g. 'type:book'
  6. */
  7. public function filter_extract(&$filterstring, $name) {
  8. $extracted = array();
  9. // Range queries. The "TO" is case-sensitive.
  10. $patterns[] = '/(^| |-)'. $name .':([\[\{](\S+) TO (\S+)[\]\}])/';
  11. // Match quoted values.
  12. $patterns[] = '/(^| |-)'. $name .':"([^"]*)"/';
  13. // Match unquoted values.
  14. $patterns[] = '/(^| |-)'. $name .':([^ ]*)/';
  15. foreach ($patterns as $p) {
  16. if (preg_match_all($p, $filterstring, $matches, PREG_SET_ORDER)) {
  17. foreach($matches as $match) {
  18. $filter = array();
  19. $filter['#query'] = $match[0];
  20. $filter['#exclude'] = ($match[1] == '-');
  21. $filter['#value'] = trim($match[2]);
  22. if (isset($match[3])) {
  23. // Extra data for range queries
  24. $filter['#start'] = $match[3];
  25. $filter['#end'] = $match[4];
  26. }
  27. $extracted[] = $filter;
  28. // Update the local copy of $filters by removing the match.
  29. $filterstring = str_replace($match[0], '', $filterstring);
  30. }
  31. }
  32. }
  33. return $extracted;
  34. }
  35. /**
  36. * Takes an array $field and combines the #name and #value in a way
  37. * suitable for use in a Solr query.
  38. */
  39. public function make_filter(array $filter) {
  40. // If the field value has spaces, or : in it, wrap it in double quotes.
  41. // unless it is a range query.
  42. if (preg_match('/[ :]/', $filter['#value']) && !isset($filter['#start']) && !preg_match('/[\[\{]\S+ TO \S+[\]\}]/', $filter['#value'])) {
  43. $filter['#value'] = '"'. $filter['#value']. '"';
  44. }
  45. $prefix = empty($filter['#exclude']) ? '' : '-';
  46. return $prefix . $filter['#name'] . ':' . $filter['#value'];
  47. }
  48. /**
  49. * Static shared by all instances, used to increment ID numbers.
  50. */
  51. protected static $idCount = 0;
  52. /**
  53. * Each query/subquery will have a unique ID
  54. */
  55. public $id;
  56. /**
  57. * A keyed array where the key is a position integer and the value
  58. * is an array with #name and #value properties. Each value is a
  59. * used for filter queries, e.g. array('#name' => 'uid', '#value' => 0)
  60. * for anonymous content.
  61. */
  62. protected $fields;
  63. /**
  64. * The complete filter string for a query. Usually from $_GET['filters']
  65. * Contains name:value pairs for filter queries. For example,
  66. * "type:book" for book nodes.
  67. */
  68. protected $filterstring;
  69. /**
  70. * A mapping of field names from the URL to real index field names.
  71. */
  72. protected $field_map = array();
  73. /**
  74. * An array of subqueries.
  75. */
  76. protected $subqueries = array();
  77. /**
  78. * The search keywords.
  79. */
  80. protected $keys;
  81. /**
  82. * The search base path.
  83. */
  84. protected $base_path;
  85. /**
  86. * Apache_Solr_Service object
  87. */
  88. protected $solr;
  89. protected $available_sorts;
  90. // Makes sure we always have a valid sort.
  91. protected $solrsort = array('#name' => 'score', '#direction' => 'asc');
  92. /**
  93. * @param $solr
  94. * An instantiated Apache_Solr_Service Object.
  95. * Can be instantiated from apachesolr_get_solr().
  96. *
  97. * @param $keys
  98. * The string that a user would type into the search box. Suitable input
  99. * may come from search_get_keys().
  100. *
  101. * @param $filterstring
  102. * Key and value pairs that are applied as filter queries.
  103. *
  104. * @param $sortstring
  105. * Visible string telling solr how to sort - added to GET query params.
  106. *
  107. * @param $base_path
  108. * The search base path (without the keywords) for this query.
  109. */
  110. function __construct($solr, $keys, $filterstring, $sortstring, $base_path) {
  111. $this->solr = $solr;
  112. $this->keys = trim($keys);
  113. $this->filterstring = trim($filterstring);
  114. $this->parse_filters();
  115. $this->available_sorts = $this->default_sorts();
  116. $this->sortstring = trim($sortstring);
  117. $this->parse_sortstring();
  118. $this->base_path = $base_path;
  119. $this->id = ++self::$idCount;
  120. }
  121. function __clone() {
  122. $this->id = ++self::$idCount;
  123. }
  124. public function add_filter($field, $value, $exclude = FALSE, $callbacks = array()) {
  125. $this->fields[] = array('#exclude' => $exclude, '#name' => $field, '#value' => trim($value), '#callbacks' => $callbacks);
  126. }
  127. /**
  128. * Get all filters, or the subset of filters for one field.
  129. *
  130. * @param $name
  131. * Optional name of a Solr field.
  132. */
  133. public function get_filters($name = NULL) {
  134. if (empty($name)) {
  135. return $this->fields;
  136. }
  137. reset($this->fields);
  138. $matches = array();
  139. foreach ($this->fields as $filter) {
  140. if ($filter['#name'] == $name) {
  141. $matches[] = $filter;
  142. }
  143. }
  144. return $matches;
  145. }
  146. public function remove_filter($name, $value = NULL) {
  147. // We can only remove named fields.
  148. if (empty($name)) {
  149. return;
  150. }
  151. if (!isset($value)) {
  152. foreach ($this->fields as $pos => $values) {
  153. if ($values['#name'] == $name) {
  154. unset($this->fields[$pos]);
  155. }
  156. }
  157. }
  158. else {
  159. foreach ($this->fields as $pos => $values) {
  160. if ($values['#name'] == $name && $values['#value'] == $value) {
  161. unset($this->fields[$pos]);
  162. }
  163. }
  164. }
  165. }
  166. public function has_filter($name, $value) {
  167. foreach ($this->fields as $pos => $values) {
  168. if (isset($values['#name']) && isset($values['#value']) && $values['#name'] == $name && $values['#value'] == $value) {
  169. return TRUE;
  170. }
  171. }
  172. return FALSE;
  173. }
  174. /**
  175. * Handle aliases for field to make nicer URLs
  176. *
  177. * @param $field_map
  178. * An array keyed with real Solr index field names, with value being the alias.
  179. */
  180. function add_field_aliases($field_map) {
  181. $this->field_map = array_merge($this->field_map, $field_map);
  182. // We have to re-parse the filters.
  183. $this->parse_filters();
  184. }
  185. function get_field_aliases() {
  186. return $this->field_map;
  187. }
  188. function clear_field_aliases() {
  189. $this->field_map = array();
  190. // We have to re-parse the filters.
  191. $this->parse_filters();
  192. }
  193. /**
  194. * Set keywords in this query.
  195. *
  196. * @param $keys
  197. * New keywords
  198. */
  199. function set_keys($keys) {
  200. $this->keys = $keys;
  201. }
  202. /**
  203. * Get this query's keywords.
  204. */
  205. function get_keys() {
  206. return $this->keys;
  207. }
  208. /**
  209. * A subquery is another instance of a Solr_Base_Query that should be joined
  210. * to the query. The operator determines whether it will be joined with AND or
  211. * OR.
  212. *
  213. * @param $query
  214. * An instance of Drupal_Solr_Query_Interface.
  215. *
  216. * @param $operator
  217. * 'AND' or 'OR'
  218. */
  219. public function add_subquery(Drupal_Solr_Query_Interface $query, $fq_operator = 'OR', $q_operator = 'AND') {
  220. $this->subqueries[$query->id] = array('#query' => $query, '#fq_operator' => $fq_operator, '#q_operator' => $q_operator);
  221. }
  222. public function remove_subquery(Drupal_Solr_Query_Interface $query) {
  223. unset($this->subqueries[$query->id]);
  224. }
  225. public function remove_subqueries() {
  226. $this->subqueries = array();
  227. }
  228. protected function parse_sortstring() {
  229. // Substitute any field aliases with real field names.
  230. $sortstring = strtr($this->sortstring, array_flip($this->field_map));
  231. // Score is a special case - it's the default sort for Solr.
  232. if ('' == $sortstring) {
  233. $this->set_solrsort('score', 'asc');
  234. }
  235. else {
  236. // Validate and set sort parameter
  237. $fields = implode('|', array_keys($this->available_sorts));
  238. if (preg_match('/^(?:('. $fields .') (asc|desc),?)+$/', $sortstring, $matches)) {
  239. // We only use the last match.
  240. $this->set_solrsort($matches[1], $matches[2]);
  241. }
  242. }
  243. }
  244. public function set_solrsort($name, $direction) {
  245. if (isset($this->available_sorts[$name])) {
  246. $this->solrsort = array('#name' => $name, '#direction' => $direction);
  247. }
  248. }
  249. public function get_solrsort() {
  250. return $this->solrsort;
  251. }
  252. public function get_available_sorts() {
  253. return $this->available_sorts;
  254. }
  255. public function set_available_sort($name, $sort) {
  256. // We expect non-aliased sorts to be added.
  257. $this->available_sorts[$name] = $sort;
  258. // Re-parse the sortstring.
  259. $this->parse_sortstring();
  260. }
  261. public function remove_available_sort($name) {
  262. unset($this->available_sorts[$name]);
  263. // Re-parse the sortstring.
  264. $this->parse_sortstring();
  265. }
  266. /**
  267. * Returns a default list of sorts.
  268. */
  269. protected function default_sorts() {
  270. // The array keys must always be real Solr index fields.
  271. return array(
  272. 'score' => array('title' => t('Relevancy'), 'default' => 'asc'),
  273. 'sort_title' => array('title' => t('Title'), 'default' => 'asc'),
  274. 'type' => array('title' => t('Type'), 'default' => 'asc'),
  275. 'sort_name' => array('title' => t('Author'), 'default' => 'asc'),
  276. 'created' => array('title' => t('Date'), 'default' => 'desc'),
  277. );
  278. }
  279. /**
  280. * Return filters and sort in a form suitable for a query param to url().
  281. */
  282. public function get_url_queryvalues() {
  283. $queryvalues = array();
  284. if ($fq = $this->rebuild_fq(TRUE)) {
  285. foreach ($fq as $delta => $values) {
  286. $queryvalues['filters'] .= ' ' . implode(' ', $values);
  287. }
  288. }
  289. if (isset($queryvalues['filters'])) {
  290. $queryvalues['filters'] = trim($queryvalues['filters']);
  291. }
  292. $solrsort = $this->solrsort;
  293. if ($solrsort && ($solrsort['#name'] != 'score' || $solrsort['#direction'] != 'asc')) {
  294. if (isset($this->field_map[$solrsort['#name']])) {
  295. $solrsort['#name'] = $this->field_map[$solrsort['#name']];
  296. }
  297. $queryvalues['solrsort'] = $solrsort['#name'] .' '. $solrsort['#direction'];
  298. }
  299. return $queryvalues;
  300. }
  301. public function get_fq() {
  302. return $this->rebuild_fq();
  303. }
  304. /**
  305. * A function to get just the keyword components of the query,
  306. * omitting any field:value portions.
  307. */
  308. public function get_query_basic() {
  309. return $this->rebuild_query();
  310. }
  311. /**
  312. * Return the search path.
  313. *
  314. * @param string $new_keywords
  315. * Optional. When set, this string overrides the query's current keywords.
  316. */
  317. public function get_path($new_keywords = NULL) {
  318. if (isset($new_keywords)) {
  319. return $this->base_path . '/' . $new_keywords;
  320. }
  321. return $this->base_path . '/' . $this->get_query_basic();
  322. }
  323. /**
  324. * Build additional breadcrumb elements relative to $base.
  325. */
  326. public function get_breadcrumb($base = NULL) {
  327. $progressive_crumb = array();
  328. if (!isset($base)) {
  329. $base = $this->get_path();
  330. }
  331. $search_keys = $this->get_query_basic();
  332. if ($search_keys) {
  333. $breadcrumb[] = l($search_keys, $base);
  334. }
  335. foreach ($this->fields as $field) {
  336. $name = $field['#name'];
  337. // Look for a field alias.
  338. if (isset($this->field_map[$name])) {
  339. $field['#name'] = $this->field_map[$name];
  340. }
  341. $progressive_crumb[] = $this->make_filter($field);
  342. $options = array('query' => 'filters=' . rawurlencode(implode(' ', $progressive_crumb)));
  343. $breadcrumb_name = "apachesolr_breadcrumb_" . $name;
  344. // Modules utilize this alter to consolidate several fields into one
  345. // theme function. This is how CCK breadcrumbs are handled.
  346. drupal_alter('apachesolr_theme_breadcrumb', $breadcrumb_name);
  347. if ($themed = theme($breadcrumb_name, $field)) {
  348. $breadcrumb[] = l($themed, $base, $options);
  349. }
  350. else {
  351. $breadcrumb[] = l($field['#value'], $base, $options);
  352. }
  353. }
  354. // The last breadcrumb is the current page, so it shouldn't be a link.
  355. $last = count($breadcrumb) - 1;
  356. $breadcrumb[$last] = strip_tags($breadcrumb[$last]);
  357. return $breadcrumb;
  358. }
  359. /**
  360. * Parse the filter string in $this->filters into $this->fields.
  361. *
  362. * Builds an array of field name/value pairs.
  363. */
  364. protected function parse_filters() {
  365. $this->fields = array();
  366. $filterstring = $this->filterstring;
  367. // Gets information about the fields already in solr index.
  368. $index_fields = $this->solr->getFields();
  369. foreach ((array) $index_fields as $name => $data) {
  370. // Look for a field alias.
  371. $alias = isset($this->field_map[$name]) ? $this->field_map[$name] : $name;
  372. // Get the values for $name
  373. $extracted = $this->filter_extract($filterstring, $alias);
  374. if (count($extracted)) {
  375. foreach ($extracted as $filter) {
  376. $pos = strpos($this->filterstring, $filter['#query']);
  377. // $solr_keys and $solr_crumbs are keyed on $pos so that query order
  378. // is maintained. This is important for breadcrumbs.
  379. $filter['#name'] = $name;
  380. $this->fields[$pos] = $filter;
  381. }
  382. }
  383. }
  384. // Even though the array has the right keys they are likely in the wrong
  385. // order. ksort() sorts the array by key while maintaining the key.
  386. ksort($this->fields);
  387. }
  388. /**
  389. * Builds a set of filter queries from $this->fields and all subqueries.
  390. *
  391. * Returns an array of strings that can be combined into
  392. * a URL query parameter or passed to Solr as fq paramters.
  393. */
  394. protected function rebuild_fq($aliases = FALSE) {
  395. $fq = array();
  396. $fields = array();
  397. foreach ($this->fields as $pos => $field) {
  398. // Look for a field alias.
  399. if ($aliases && isset($this->field_map[$field['#name']])) {
  400. $field['#name'] = $this->field_map[$field['#name']];
  401. }
  402. $fq[$field['#name']][] = $this->make_filter($field);
  403. }
  404. foreach ($this->subqueries as $id => $data) {
  405. $subfq = $data['#query']->rebuild_fq($aliases);
  406. if ($subfq) {
  407. $operator = $data['#fq_operator'];
  408. $subqueries = array();
  409. foreach ($subfq as $key => $values) {
  410. foreach ($values as $value) {
  411. $subqueries[] = $value;
  412. }
  413. }
  414. $fq['subqueries'][$key] = " {$data['#q_operataor']} (" . implode(" $operator " , $subqueries) . ")";
  415. }
  416. }
  417. return $fq;
  418. }
  419. protected function rebuild_query() {
  420. $query = $this->keys;
  421. foreach ($this->subqueries as $id => $data) {
  422. $operator = $data['#q_operator'];
  423. $subquery = $data['#query']->get_query_basic();
  424. if ($subquery) {
  425. $query .= " {$operator} ({$subquery})";
  426. }
  427. }
  428. return $query;
  429. }
  430. }