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