first import
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
Search facets
|
||||
-------------
|
||||
|
||||
This module allows you to create facetted searches for any search executed via
|
||||
the Search API, no matter if executed by a search page, a view or any other
|
||||
module. The only thing you'll need is a search service class that supports the
|
||||
"search_api_facets" feature. Currently, the "Database search" and "Solr search"
|
||||
modules supports this.
|
||||
|
||||
This module is built on the Facet API [1], which is needed for this module to
|
||||
work.
|
||||
|
||||
[1] http://drupal.org/project/facetapi
|
||||
|
||||
|
||||
Information for site builders
|
||||
-----------------------------
|
||||
|
||||
For creating a facetted search, you first need a search. Create or find some
|
||||
page displaying Search API search results, either via a search page, a view or
|
||||
by any other means. Now go to the configuration page for the index on which
|
||||
this search is executed.
|
||||
If the index lies on a server supporting facets (and if this module is enabled),
|
||||
you'll notice a "Facets" tab. Click it and it will take you to the index' facet
|
||||
configuration page. You'll see a table containing all indexed fields and options
|
||||
for enabling and configuring facets for them.
|
||||
For a detailed explanation of the available options, please refer to the Facet
|
||||
API documentation.
|
||||
|
||||
- Creating facets via the URL
|
||||
|
||||
Facets can be added to a search (for which facets are activated) by passing
|
||||
appropriate GET parameters in the URL. Assuming you have an indexed field with
|
||||
the machine name "field_price", you can filter on it in the following ways:
|
||||
|
||||
- Filter for a specific value. For finding only results that have a price of
|
||||
exactly 100, pass the following $options to url() or l():
|
||||
|
||||
$options['query']['f'][] = 'field_price:100';
|
||||
|
||||
Or manually append the following GET parameter to a URL:
|
||||
|
||||
?f[0]=field_price:100
|
||||
|
||||
- Search for values in a specified range. The following example will only return
|
||||
items that have a price greater than or equal to 100 and lower than 500.
|
||||
|
||||
Code: $options['query']['f'][] = 'field_price:[100 TO 500]';
|
||||
URL: ?f[0]=field_price%3A%5B100%20TO%20500%5D
|
||||
|
||||
- Search for values above a value. The next example will find results which have
|
||||
a price greater than or equal to 100. The asterisk (*) stands for "unlimited",
|
||||
meaning that there is no upper limit. Filtering for values lower than a
|
||||
certain value works equivalently.
|
||||
|
||||
Code: $options['query']['f'][] = 'field_price:[100 TO *]';
|
||||
URL: ?f[0]=field_price%3A%5B100%20TO%20%2A%5D
|
||||
|
||||
- Search for missing values. This example will filter out all items which have
|
||||
any value at all in the price field, and will therefore only list items on
|
||||
which this field was omitted. (This naturally only makes sense for fields
|
||||
that aren't required.)
|
||||
|
||||
Code: $options['query']['f'][] = 'field_price:!';
|
||||
URL: ?f[0]=field_price%3A%21
|
||||
|
||||
- Search for present values. The following example will only return items which
|
||||
have the price field set (regardless of the actual value). You can see that it
|
||||
is actually just a range filter with unlimited lower and upper bound.
|
||||
|
||||
Code: $options['query']['f'][] = 'field_price:[* TO *]';
|
||||
URL: ?f[0]=field_price%3A%5B%2A%20TO%20%2A%5D
|
||||
|
||||
Note: When filtering a field whose machine name contains a colon (e.g.,
|
||||
"author:roles"), you'll have to additionally URL-encode the field name in these
|
||||
filter values:
|
||||
Code: $options['query']['f'][] = rawurlencode('author:roles') . ':100';
|
||||
URL: ?f[0]=author%253Aroles%3A100
|
||||
|
||||
- Issues
|
||||
|
||||
If you find any bugs or shortcomings while using this module, please file an
|
||||
issue in the project's issue queue [1], using the "Facets" component.
|
||||
|
||||
[1] http://drupal.org/project/issues/search_api
|
||||
|
||||
|
||||
Information for developers
|
||||
--------------------------
|
||||
|
||||
- Features
|
||||
|
||||
If you are the developer of a SearchApiServiceInterface implementation and want
|
||||
to support facets with your service class, too, you'll have to support the
|
||||
"search_api_facets" feature. You can find details about the necessary additions
|
||||
to your class in the example_servive.php file. In short, you'll just, when
|
||||
executing a query, have to return facet terms and counts according to the
|
||||
query's "search_api_facets" option, if present.
|
||||
In order for the module to be able to tell that your server supports facets,
|
||||
you will also have to change your service's supportsFeature() method to
|
||||
something like the following:
|
||||
public function supportsFeature($feature) {
|
||||
return $feature == 'search_api_facets';
|
||||
}
|
||||
|
||||
There is also a second feature defined by this module, namely
|
||||
"search_api_facets_operator_or", for supporting "OR" facets. The requirements
|
||||
for this feature are also explained in the example_servive.php file.
|
||||
|
||||
- Query option
|
||||
|
||||
The facets created follow the "search_api_base_path" option on the search query.
|
||||
If set, this path will be used as the base path from which facet links will be
|
||||
created. This can be used to show facets on pages without searches – e.g., as a
|
||||
landing page.
|
||||
|
||||
- Hidden variable
|
||||
|
||||
The module uses one hidden variable, "search_api_facets_search_ids", to keep
|
||||
track of the search IDs of searches executed for a given index. It is only
|
||||
updated when a facet is displayed for the respective search, so isn't really a
|
||||
reliable measure for this.
|
||||
In any case, if you e.g. did some test searches and now don't want them to show
|
||||
up in the block configuration forever after, just clear the variable:
|
||||
variable_del("search_api_facets_search_ids")
|
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Example implementation for a service class which supports facets.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Example class explaining how facets can be supported by a service class.
|
||||
*
|
||||
* This class defines the "search_api_facets" and
|
||||
* "search_api_facets_operator_or" features. Read the method documentation and
|
||||
* inline comments in search() to learn how they can be supported by a service
|
||||
* class.
|
||||
*/
|
||||
abstract class SearchApiFacetapiExampleService extends SearchApiAbstractService {
|
||||
|
||||
/**
|
||||
* Determines whether this service class implementation supports a given
|
||||
* feature. Features are optional extensions to Search API functionality and
|
||||
* usually defined and used by third-party modules.
|
||||
*
|
||||
* If the service class supports facets, it should return TRUE if called with
|
||||
* the feature name "search_api_facets". If it also supports "OR" facets, it
|
||||
* should also return TRUE if called with "search_api_facets_operator_or".
|
||||
*
|
||||
* @param string $feature
|
||||
* The name of the optional feature.
|
||||
*
|
||||
* @return boolean
|
||||
* TRUE if this service knows and supports the specified feature. FALSE
|
||||
* otherwise.
|
||||
*/
|
||||
public function supportsFeature($feature) {
|
||||
$supported = array(
|
||||
'search_api_facets' => TRUE,
|
||||
'search_api_facets_operator_or' => TRUE,
|
||||
);
|
||||
return isset($supported[$feature]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a search on the server represented by this object.
|
||||
*
|
||||
* If the service class supports facets, it should check for an additional
|
||||
* option on the query object:
|
||||
* - search_api_facets: An array of facets to return along with the results
|
||||
* for this query. The array is keyed by an arbitrary string which should
|
||||
* serve as the facet's unique identifier for this search. The values are
|
||||
* arrays with the following keys:
|
||||
* - field: The field to construct facets for.
|
||||
* - limit: The maximum number of facet terms to return. 0 or an empty
|
||||
* value means no limit.
|
||||
* - min_count: The minimum number of results a facet value has to have in
|
||||
* order to be returned.
|
||||
* - missing: If TRUE, a facet for all items with no value for this field
|
||||
* should be returned (if it conforms to limit and min_count).
|
||||
* - operator: (optional) If the service supports "OR" facets and this key
|
||||
* contains the string "or", the returned facets should be "OR" facets. If
|
||||
* the server doesn't support "OR" facets, this key can be ignored.
|
||||
*
|
||||
* The basic principle of facets is explained quite well in the
|
||||
* @link http://en.wikipedia.org/wiki/Faceted_search Wikipedia article on
|
||||
* "Faceted search" @endlink. Basically, you should return for each field
|
||||
* filter values which would yield some results when used with the search.
|
||||
* E.g., if you return for a field $field the term $term with $count results,
|
||||
* the given $query along with
|
||||
* $query->condition($field, $term)
|
||||
* should yield exactly (or about) $count results.
|
||||
*
|
||||
* For "OR" facets, all existing filters on the facetted field should be
|
||||
* ignored for computing the facets.
|
||||
*
|
||||
* @param $query
|
||||
* The SearchApiQueryInterface object to execute.
|
||||
*
|
||||
* @return array
|
||||
* An associative array containing the search results, as required by
|
||||
* SearchApiQueryInterface::execute().
|
||||
* In addition, if the "search_api_facets" option is present on the query,
|
||||
* the results should contain an array of facets in the "search_api_facets"
|
||||
* key, as specified by the option. The facets array should be keyed by the
|
||||
* facets' unique identifiers, and contain a numeric array of facet terms,
|
||||
* sorted descending by result count. A term is represented by an array with
|
||||
* the following keys:
|
||||
* - count: Number of results for this term.
|
||||
* - filter: The filter to apply when selecting this facet term. A filter is
|
||||
* a string of one of the following forms:
|
||||
* - "VALUE": Filter by the literal value VALUE (always include the
|
||||
* quotes, not only for strings).
|
||||
* - [VALUE1 VALUE2]: Filter for a value between VALUE1 and VALUE2. Use
|
||||
* parantheses for excluding the border values and square brackets for
|
||||
* including them. An asterisk (*) can be used as a wildcard. E.g.,
|
||||
* (* 0) or [* 0) would be a filter for all negative values.
|
||||
* - !: Filter for items without a value for this field (i.e., the
|
||||
* "missing" facet).
|
||||
*
|
||||
* @throws SearchApiException
|
||||
* If an error prevented the search from completing.
|
||||
*/
|
||||
public function search(SearchApiQueryInterface $query) {
|
||||
// We assume here that we have an AI search which understands English
|
||||
// commands.
|
||||
|
||||
// First, create the normal search query, without facets.
|
||||
$search = new SuperCoolAiSearch($query->getIndex());
|
||||
$search->cmd('create basic search for the following query', $query);
|
||||
$ret = $search->cmd('return search results in Search API format');
|
||||
|
||||
// Then, let's see if we should return any facets.
|
||||
if ($facets = $query->getOption('search_api_facets')) {
|
||||
// For the facets, we need all results, not only those in the specified
|
||||
// range.
|
||||
$results = $search->cmd('return unlimited search results as a set');
|
||||
foreach ($facets as $id => $facet) {
|
||||
$field = $facet['field'];
|
||||
$limit = empty($facet['limit']) ? 'all' : $facet['limit'];
|
||||
$min_count = $facet['min_count'];
|
||||
$missing = $facet['missing'];
|
||||
$or = isset($facet['operator']) && $facet['operator'] == 'or';
|
||||
|
||||
// If this is an "OR" facet, existing filters on the field should be
|
||||
// ignored for computing the facets.
|
||||
// You can ignore this if your service class doesn't support the
|
||||
// "search_api_facets_operator_or" feature.
|
||||
if ($or) {
|
||||
// We have to execute another query (in the case of this hypothetical
|
||||
// search backend, at least) to get the right result set to facet.
|
||||
$tmp_search = new SuperCoolAiSearch($query->getIndex());
|
||||
$tmp_search->cmd('create basic search for the following query', $query);
|
||||
$tmp_search->cmd("remove all conditions for field $field");
|
||||
$tmp_results = $tmp_search->cmd('return unlimited search results as a set');
|
||||
}
|
||||
else {
|
||||
// Otherwise, we can just use the normal results.
|
||||
$tmp_results = $results;
|
||||
}
|
||||
|
||||
$filters = array();
|
||||
if ($search->cmd("$field is a date or numeric field")) {
|
||||
// For date, integer or float fields, range facets are more useful.
|
||||
$ranges = $search->cmd("list $limit ranges of field $field in the following set", $tmp_results);
|
||||
foreach ($ranges as $range) {
|
||||
if ($range->getCount() >= $min_count) {
|
||||
// Get the lower and upper bound of the range. * means unlimited.
|
||||
$lower = $range->getLowerBound();
|
||||
$lower = ($lower == SuperCoolAiSearch::RANGE_UNLIMITED) ? '*' : $lower;
|
||||
$upper = $range->getUpperBound();
|
||||
$upper = ($upper == SuperCoolAiSearch::RANGE_UNLIMITED) ? '*' : $upper;
|
||||
// Then, see whether the bounds are included in the range. These
|
||||
// can be specified independently for the lower and upper bound.
|
||||
// Parentheses are used for exclusive bounds, square brackets are
|
||||
// used for inclusive bounds.
|
||||
$lowChar = $range->isLowerBoundInclusive() ? '[' : '(';
|
||||
$upChar = $range->isUpperBoundInclusive() ? ']' : ')';
|
||||
// Create the filter, which separates the bounds with a single
|
||||
// space.
|
||||
$filter = "$lowChar$lower $upper$upChar";
|
||||
$filters[$filter] = $range->getCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Otherwise, we use normal single-valued facets.
|
||||
$terms = $search->cmd("list $limit values of field $field in the following set", $tmp_results);
|
||||
foreach ($terms as $term) {
|
||||
if ($term->getCount() >= $min_count) {
|
||||
// For single-valued terms, we just need to wrap them in quotes.
|
||||
$filter = '"' . $term->getValue() . '"';
|
||||
$filters[$filter] = $term->getCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we should also return a "missing" facet, compute that as the
|
||||
// number of results without a value for the facet field.
|
||||
if ($missing) {
|
||||
$count = $search->cmd("return number of results without field $field in the following set", $tmp_results);
|
||||
if ($count >= $min_count) {
|
||||
$filters['!'] = $count;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the facets descending by result count.
|
||||
arsort($filters);
|
||||
|
||||
// With the "missing" facet, we might have too many facet terms (unless
|
||||
// $limit was empty and is therefore now set to "all"). If this is the
|
||||
// case, remove those with the lowest number of results.
|
||||
while (is_numeric($limit) && count($filters) > $limit) {
|
||||
array_pop($filters);
|
||||
}
|
||||
|
||||
// Now add the facet terms to the return value, as specified in the doc
|
||||
// comment for this method.
|
||||
foreach ($filters as $filter => $count) {
|
||||
$ret['search_api_facets'][$id][] = array(
|
||||
'count' => $count,
|
||||
'filter' => $filter,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the results, which now also includes the facet information.
|
||||
return $ret;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Classes used by the Facet API module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Facet API adapter for the Search API module.
|
||||
*/
|
||||
class SearchApiFacetapiAdapter extends FacetapiAdapter {
|
||||
|
||||
/**
|
||||
* Cached value for the current search for this searcher, if any.
|
||||
*
|
||||
* @see getCurrentSearch()
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $current_search;
|
||||
|
||||
/**
|
||||
* The active facet fields for the current search.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fields = array();
|
||||
|
||||
/**
|
||||
* Returns the path to the admin settings for a given realm.
|
||||
*
|
||||
* @param $realm_name
|
||||
* The name of the realm.
|
||||
*
|
||||
* @return
|
||||
* The path to the admin settings.
|
||||
*/
|
||||
public function getPath($realm_name) {
|
||||
$base_path = 'admin/config/search/search_api';
|
||||
$index_id = $this->info['instance'];
|
||||
return $base_path . '/index/' . $index_id . '/facets/' . $realm_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides FacetapiAdapter::getSearchPath().
|
||||
*/
|
||||
public function getSearchPath() {
|
||||
$search = $this->getCurrentSearch();
|
||||
if ($search && $search[0]->getOption('search_api_base_path')) {
|
||||
return $search[0]->getOption('search_api_base_path');
|
||||
}
|
||||
return $_GET['q'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the backend to initialize its query object before adding the facet filters.
|
||||
*
|
||||
* @param mixed $query
|
||||
* The backend's native object.
|
||||
*/
|
||||
public function initActiveFilters($query) {
|
||||
$search_id = $query->getOption('search id');
|
||||
$index_id = $this->info['instance'];
|
||||
$facets = facetapi_get_enabled_facets($this->info['name']);
|
||||
$this->fields = array();
|
||||
|
||||
// We statically store the current search per facet so that we can correctly
|
||||
// assign it when building the facets. See the build() method in the query
|
||||
// type plugin classes.
|
||||
$active = &drupal_static('search_api_facetapi_active_facets', array());
|
||||
foreach ($facets as $facet) {
|
||||
$options = $this->getFacet($facet)->getSettings()->settings;
|
||||
// The 'default_true' option is a choice between "show on all but the
|
||||
// selected searches" (TRUE) and "show for only the selected searches".
|
||||
$default_true = isset($options['default_true']) ? $options['default_true'] : TRUE;
|
||||
// The 'facet_search_ids' option is the list of selected searches that
|
||||
// will either be excluded or for which the facet will exclusively be
|
||||
// displayed.
|
||||
$facet_search_ids = isset($options['facet_search_ids']) ? $options['facet_search_ids'] : array();
|
||||
|
||||
if (array_search($search_id, $facet_search_ids) === FALSE) {
|
||||
$search_ids = variable_get('search_api_facets_search_ids', array());
|
||||
if (empty($search_ids[$index_id][$search_id])) {
|
||||
// Remember this search ID.
|
||||
$search_ids[$index_id][$search_id] = $search_id;
|
||||
variable_set('search_api_facets_search_ids', $search_ids);
|
||||
}
|
||||
if (!$default_true) {
|
||||
continue; // We are only to show facets for explicitly named search ids.
|
||||
}
|
||||
}
|
||||
elseif ($default_true) {
|
||||
continue; // The 'facet_search_ids' in the settings are to be excluded.
|
||||
}
|
||||
$active[$facet['name']] = $search_id;
|
||||
$this->fields[$facet['name']] = array(
|
||||
'field' => $facet['field'],
|
||||
'limit' => $options['hard_limit'],
|
||||
'operator' => $options['operator'],
|
||||
'min_count' => $options['facet_mincount'],
|
||||
'missing' => $options['facet_missing'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given facet to the query.
|
||||
*/
|
||||
public function addFacet(array $facet, SearchApiQueryInterface $query) {
|
||||
if (isset($this->fields[$facet['name']])) {
|
||||
$options = &$query->getOptions();
|
||||
$options['search_api_facets'][$facet['name']] = $this->fields[$facet['name']];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean flagging whether $this->_searcher executed a search.
|
||||
*/
|
||||
public function searchExecuted() {
|
||||
return (bool) $this->getCurrentSearch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for getting a current search for this searcher.
|
||||
*
|
||||
* @return array
|
||||
* The first matching current search, in the form specified by
|
||||
* search_api_current_search(). Or NULL, if no match was found.
|
||||
*/
|
||||
public function getCurrentSearch() {
|
||||
if (!isset($this->current_search)) {
|
||||
$this->current_search = FALSE;
|
||||
$index_id = $this->info['instance'];
|
||||
// There is currently no way to configure the "current search" block to
|
||||
// show on a per-searcher basis as we do with the facets. Therefore we
|
||||
// cannot match it up to the correct "current search".
|
||||
// I suspect that http://drupal.org/node/593658 would help.
|
||||
// For now, just taking the first current search for this index. :-/
|
||||
foreach (search_api_current_search() as $search) {
|
||||
list($query, $results) = $search;
|
||||
if ($query->getIndex()->machine_name == $index_id) {
|
||||
$this->current_search = $search;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $this->current_search ? $this->current_search : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean flagging whether facets in a realm shoud be displayed.
|
||||
*
|
||||
* Useful, for example, for suppressing sidebar blocks in some cases.
|
||||
*
|
||||
* @return
|
||||
* A boolean flagging whether to display a given realm.
|
||||
*/
|
||||
public function suppressOutput($realm_name) {
|
||||
// Not sure under what circumstances the output will need to be suppressed?
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the search keys.
|
||||
*/
|
||||
public function getSearchKeys() {
|
||||
$search = $this->getCurrentSearch();
|
||||
$keys = $search[0]->getOriginalKeys();
|
||||
if (is_array($keys)) {
|
||||
// This will happen nearly never when displaying the search keys to the
|
||||
// user, so go with a simple work-around.
|
||||
// If someone complains, we can easily add a method for printing them
|
||||
// properly.
|
||||
$keys = '[' . t('complex query') . ']';
|
||||
}
|
||||
elseif (!$keys) {
|
||||
// If a base path other than the current one is set, we assume that we
|
||||
// shouldn't report on the current search. Highly hack-y, of course.
|
||||
if ($search[0]->getOption('search_api_base_path', $_GET['q']) !== $_GET['q']) {
|
||||
return NULL;
|
||||
}
|
||||
// Work-around since Facet API won't show the "Current search" block
|
||||
// without keys.
|
||||
$keys = '[' . t('all items') . ']';
|
||||
}
|
||||
drupal_alter('search_api_facetapi_keys', $keys, $search[0]);
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of total results found for the current search.
|
||||
*/
|
||||
public function getResultCount() {
|
||||
$search = $this->getCurrentSearch();
|
||||
// Each search is an array with the query as the first element and the results
|
||||
// array as the second.
|
||||
if (isset($search[1])) {
|
||||
return $search[1]['result count'];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows for backend specific overrides to the settings form.
|
||||
*/
|
||||
public function settingsForm(&$form, &$form_state) {
|
||||
$facet = $form['#facetapi']['facet'];
|
||||
$realm = $form['#facetapi']['realm'];
|
||||
$facet_settings = $this->getFacet($facet)->getSettings();
|
||||
$options = $facet_settings->settings;
|
||||
$search_ids = variable_get('search_api_facets_search_ids', array());
|
||||
$search_ids = isset($search_ids[$this->info['instance']]) ? $search_ids[$this->info['instance']] : array();
|
||||
if (count($search_ids) > 1) {
|
||||
$form['global']['default_true'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Display for searches'),
|
||||
'#options' => array(
|
||||
TRUE => t('For all except the selected'),
|
||||
FALSE => t('Only for the selected'),
|
||||
),
|
||||
'#default_value' => isset($options['default_true']) ? $options['default_true'] : TRUE,
|
||||
);
|
||||
$form['global']['facet_search_ids'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Search IDs'),
|
||||
'#options' => $search_ids,
|
||||
'#size' => min(4, count($search_ids)),
|
||||
'#multiple' => TRUE,
|
||||
'#default_value' => isset($options['facet_search_ids']) ? $options['facet_search_ids'] : array(),
|
||||
);
|
||||
}
|
||||
else {
|
||||
$form['global']['default_true'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => TRUE,
|
||||
);
|
||||
$form['global']['facet_search_ids'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => array(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Date query type plugin for the Search API adapter.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Plugin for "date" query types.
|
||||
*/
|
||||
class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQueryTypeInterface {
|
||||
|
||||
/**
|
||||
* Loads the include file containing the date API functions.
|
||||
*/
|
||||
public function __construct(FacetapiAdapter $adapter, array $facet) {
|
||||
module_load_include('date.inc', 'facetapi');
|
||||
parent::__construct($adapter, $facet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the query type associated with the plugin.
|
||||
*
|
||||
* @return string
|
||||
* The query type.
|
||||
*/
|
||||
static public function getType() {
|
||||
return 'date';
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the filter to the query object.
|
||||
*
|
||||
* @param $query
|
||||
* An object containing the query in the backend's native API.
|
||||
*/
|
||||
public function execute($query) {
|
||||
// Return terms for this facet.
|
||||
$this->adapter->addFacet($this->facet, $query);
|
||||
// Change limit to "unlimited" (-1).
|
||||
$options = &$query->getOptions();
|
||||
if (!empty($options['search_api_facets'][$this->facet['name']])) {
|
||||
$options['search_api_facets'][$this->facet['name']]['limit'] = -1;
|
||||
}
|
||||
|
||||
if ($active = $this->adapter->getActiveItems($this->facet)) {
|
||||
$item = end($active);
|
||||
$field = $this->facet['field'];
|
||||
$regex = str_replace(array('^', '$'), '', FACETAPI_REGEX_DATE);
|
||||
$filter = preg_replace_callback($regex, array($this, 'replaceDateString'), $item['value']);
|
||||
$this->addFacetFilter($query, $field, $filter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replacement callback for replacing ISO dates with timestamps.
|
||||
*/
|
||||
public function replaceDateString($matches) {
|
||||
return strtotime($matches[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the facet's build array.
|
||||
*
|
||||
* @return array
|
||||
* The initialized render array.
|
||||
*/
|
||||
public function build() {
|
||||
$facet = $this->adapter->getFacet($this->facet);
|
||||
$search_ids = drupal_static('search_api_facetapi_active_facets', array());
|
||||
if (empty($search_ids[$facet['name']]) || !search_api_current_search($search_ids[$facet['name']])) {
|
||||
return array();
|
||||
}
|
||||
$search_id = $search_ids[$facet['name']];
|
||||
$build = array();
|
||||
$search = search_api_current_search($search_id);
|
||||
$results = $search[1];
|
||||
if (!$results['result count']) {
|
||||
return array();
|
||||
}
|
||||
// Gets total number of documents matched in search.
|
||||
$total = $results['result count'];
|
||||
|
||||
// Most of the code below is copied from search_facetapi's implementation of
|
||||
// this method.
|
||||
|
||||
// Executes query, iterates over results.
|
||||
if (isset($results['search_api_facets']) && isset($results['search_api_facets'][$this->facet['field']])) {
|
||||
$values = $results['search_api_facets'][$this->facet['field']];
|
||||
foreach ($values as $value) {
|
||||
if ($value['count']) {
|
||||
$filter = $value['filter'];
|
||||
// We only process single values further. The "missing" filter and
|
||||
// range filters will be passed on unchanged.
|
||||
if ($filter == '!') {
|
||||
$build[$filter]['#count'] = $value['count'];
|
||||
}
|
||||
elseif ($filter[0] == '"') {
|
||||
$filter = substr($value['filter'], 1, -1);
|
||||
if ($filter) {
|
||||
$raw_values[$filter] = $value['count'];
|
||||
}
|
||||
}
|
||||
else {
|
||||
$filter = substr($value['filter'], 1, -1);
|
||||
$pos = strpos($filter, ' ');
|
||||
if ($pos !== FALSE) {
|
||||
$lower = facetapi_isodate(substr($filter, 0, $pos), FACETAPI_DATE_DAY);
|
||||
$upper = facetapi_isodate(substr($filter, $pos + 1), FACETAPI_DATE_DAY);
|
||||
$filter = '[' . $lower . ' TO ' . $upper . ']';
|
||||
}
|
||||
$build[$filter]['#count'] = $value['count'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gets active facets, starts building hierarchy.
|
||||
$parent = $gap = NULL;
|
||||
foreach ($this->adapter->getActiveItems($this->facet) as $value => $item) {
|
||||
// If the item is active, the count is the result set count.
|
||||
$build[$value] = array('#count' => $total);
|
||||
|
||||
// Gets next "gap" increment, minute being the lowest we can go.
|
||||
if ($value[0] != '[' || $value[strlen($value) - 1] != ']' || !($pos = strpos($value, ' TO '))) {
|
||||
continue;
|
||||
}
|
||||
$start = substr($value, 1, $pos);
|
||||
$end = substr($value, $pos + 4, -1);
|
||||
$date_gap = facetapi_get_date_gap($start, $end);
|
||||
$gap = facetapi_get_next_date_gap($date_gap, FACETAPI_DATE_MINUTE);
|
||||
|
||||
// If there is a previous item, there is a parent, uses a reference so the
|
||||
// arrays are populated when they are updated.
|
||||
if (NULL !== $parent) {
|
||||
$build[$parent]['#item_children'][$value] = &$build[$value];
|
||||
$build[$value]['#item_parents'][$parent] = $parent;
|
||||
}
|
||||
|
||||
// Stores the last value iterated over.
|
||||
$parent = $value;
|
||||
}
|
||||
if (empty($raw_values)) {
|
||||
return $build;
|
||||
}
|
||||
ksort($raw_values);
|
||||
|
||||
// Mind the gap! Calculates gap from min and max timestamps.
|
||||
$timestamps = array_keys($raw_values);
|
||||
if (NULL === $parent) {
|
||||
if (count($raw_values) > 1) {
|
||||
$gap = facetapi_get_timestamp_gap(min($timestamps), max($timestamps));
|
||||
}
|
||||
else {
|
||||
$gap = FACETAPI_DATE_HOUR;
|
||||
}
|
||||
}
|
||||
|
||||
// Converts all timestamps to dates in ISO 8601 format.
|
||||
$dates = array();
|
||||
foreach ($timestamps as $timestamp) {
|
||||
$dates[$timestamp] = facetapi_isodate($timestamp, $gap);
|
||||
}
|
||||
|
||||
// Treat each date as the range start and next date as the range end.
|
||||
$range_end = array();
|
||||
$previous = NULL;
|
||||
foreach (array_unique($dates) as $date) {
|
||||
if (NULL !== $previous) {
|
||||
$range_end[$previous] = facetapi_get_next_date_increment($previous, $gap);
|
||||
}
|
||||
$previous = $date;
|
||||
}
|
||||
$range_end[$previous] = facetapi_get_next_date_increment($previous, $gap);
|
||||
|
||||
// Groups dates by the range they belong to, builds the $build array
|
||||
// with the facet counts and formatted range values.
|
||||
foreach ($raw_values as $value => $count) {
|
||||
$new_value = '[' . $dates[$value] . ' TO ' . $range_end[$dates[$value]] . ']';
|
||||
if (!isset($build[$new_value])) {
|
||||
$build[$new_value] = array('#count' => $count);
|
||||
}
|
||||
else {
|
||||
$build[$new_value]['#count'] += $count;
|
||||
}
|
||||
|
||||
// Adds parent information if not already set.
|
||||
if (NULL !== $parent && $parent != $new_value) {
|
||||
$build[$parent]['#item_children'][$new_value] = &$build[$new_value];
|
||||
$build[$new_value]['#item_parents'][$parent] = $parent;
|
||||
}
|
||||
}
|
||||
|
||||
return $build;
|
||||
}
|
||||
}
|
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Term query type plugin for the Apache Solr adapter.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Plugin for "term" query types.
|
||||
*/
|
||||
class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTypeInterface {
|
||||
|
||||
/**
|
||||
* Returns the query type associated with the plugin.
|
||||
*
|
||||
* @return string
|
||||
* The query type.
|
||||
*/
|
||||
static public function getType() {
|
||||
return 'term';
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the filter to the query object.
|
||||
*
|
||||
* @param SearchApiQueryInterface $query
|
||||
* An object containing the query in the backend's native API.
|
||||
*/
|
||||
public function execute($query) {
|
||||
// Return terms for this facet.
|
||||
$this->adapter->addFacet($this->facet, $query);
|
||||
|
||||
$settings = $this->adapter->getFacet($this->facet)->getSettings();
|
||||
// Adds the operator parameter.
|
||||
$operator = $settings->settings['operator'];
|
||||
|
||||
// Add active facet filters.
|
||||
$active = $this->adapter->getActiveItems($this->facet);
|
||||
if (empty($active)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (FACETAPI_OPERATOR_OR == $operator) {
|
||||
// If we're dealing with an OR facet, we need to use a nested filter.
|
||||
$facet_filter = $query->createFilter('OR');
|
||||
}
|
||||
else {
|
||||
// Otherwise we set the conditions directly on the query.
|
||||
$facet_filter = $query;
|
||||
}
|
||||
|
||||
foreach ($active as $filter => $filter_array) {
|
||||
$field = $this->facet['field'];
|
||||
$this->addFacetFilter($facet_filter, $field, $filter);
|
||||
}
|
||||
|
||||
// For OR facets, we now have to add the filter to the query.
|
||||
if (FACETAPI_OPERATOR_OR == $operator) {
|
||||
$query->filter($facet_filter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for setting a facet filter on a query or query filter object.
|
||||
*/
|
||||
protected function addFacetFilter($query_filter, $field, $filter) {
|
||||
// Integer (or other nun-string) filters might mess up some of the following
|
||||
// comparison expressions.
|
||||
$filter = (string) $filter;
|
||||
if ($filter == '!') {
|
||||
$query_filter->condition($field, NULL);
|
||||
}
|
||||
elseif ($filter[0] == '[' && $filter[strlen($filter) - 1] == ']' && ($pos = strpos($filter, ' TO '))) {
|
||||
$lower = trim(substr($filter, 1, $pos));
|
||||
$upper = trim(substr($filter, $pos + 4, -1));
|
||||
if ($lower == '*' && $upper == '*') {
|
||||
$query_filter->condition($field, NULL, '<>');
|
||||
}
|
||||
else {
|
||||
if ($lower != '*') {
|
||||
// Iff we have a range with two finite boundaries, we set two
|
||||
// conditions (larger than the lower bound and less than the upper
|
||||
// bound) and therefore have to make sure that we have an AND
|
||||
// conjunction for those.
|
||||
if ($upper != '*' && !($query_filter instanceof SearchApiQueryInterface || $query_filter->getConjunction() === 'AND')) {
|
||||
$original_query_filter = $query_filter;
|
||||
$query_filter = new SearchApiQueryFilter('AND');
|
||||
}
|
||||
$query_filter->condition($field, $lower, '>=');
|
||||
}
|
||||
if ($upper != '*') {
|
||||
$query_filter->condition($field, $upper, '<=');
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
$query_filter->condition($field, $filter);
|
||||
}
|
||||
if (isset($original_query_filter)) {
|
||||
$original_query_filter->filter($query_filter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the facet's build array.
|
||||
*
|
||||
* @return array
|
||||
* The initialized render array.
|
||||
*/
|
||||
public function build() {
|
||||
$facet = $this->adapter->getFacet($this->facet);
|
||||
// The current search per facet is stored in a static variable (during
|
||||
// initActiveFilters) so that we can retrieve it here and get the correct
|
||||
// current search for this facet.
|
||||
$search_ids = drupal_static('search_api_facetapi_active_facets', array());
|
||||
if (empty($search_ids[$facet['name']]) || !search_api_current_search($search_ids[$facet['name']])) {
|
||||
return array();
|
||||
}
|
||||
$search_id = $search_ids[$facet['name']];
|
||||
$search = search_api_current_search($search_id);
|
||||
$build = array();
|
||||
$results = $search[1];
|
||||
if (isset($results['search_api_facets']) && isset($results['search_api_facets'][$this->facet['field']])) {
|
||||
$values = $results['search_api_facets'][$this->facet['field']];
|
||||
foreach ($values as $value) {
|
||||
$filter = $value['filter'];
|
||||
// As Facet API isn't really suited for our native facet filter
|
||||
// representations, convert the format here. (The missing facet can
|
||||
// stay the same.)
|
||||
if ($filter[0] == '"') {
|
||||
$filter = substr($filter, 1, -1);
|
||||
}
|
||||
elseif ($filter != '!') {
|
||||
// This is a range filter.
|
||||
$filter = substr($filter, 1, -1);
|
||||
$pos = strpos($filter, ' ');
|
||||
if ($pos !== FALSE) {
|
||||
$filter = '[' . substr($filter, 0, $pos) . ' TO ' . substr($filter, $pos + 1) . ']';
|
||||
}
|
||||
}
|
||||
$build[$filter] = array(
|
||||
'#count' => $value['count'],
|
||||
);
|
||||
}
|
||||
}
|
||||
return $build;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Hooks provided by the Search facets module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @addtogroup hooks
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Lets modules alter the search keys that are returned to FacetAPI and used
|
||||
* in the current search block and breadcrumb trail.
|
||||
*
|
||||
* @param string $keys
|
||||
* The string representing the user's current search query.
|
||||
* @param SearchApiQuery $query
|
||||
* The SearchApiQuery object for the current search.
|
||||
*/
|
||||
function hook_search_api_facetapi_keys_alter(&$keys, $query) {
|
||||
if ($keys == '[' . t('all items') . ']') {
|
||||
// Change $keys to something else, perhaps based on filters in the query
|
||||
// object.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "addtogroup hooks".
|
||||
*/
|
@@ -0,0 +1,17 @@
|
||||
name = Search facets
|
||||
description = "Integrate the Search API with the Facet API to provide facetted searches."
|
||||
dependencies[] = search_api
|
||||
dependencies[] = facetapi
|
||||
core = 7.x
|
||||
package = Search
|
||||
|
||||
files[] = plugins/facetapi/adapter.inc
|
||||
files[] = plugins/facetapi/query_type_term.inc
|
||||
files[] = plugins/facetapi/query_type_date.inc
|
||||
|
||||
; Information added by drupal.org packaging script on 2013-01-09
|
||||
version = "7.x-1.4+0-dev"
|
||||
core = "7.x"
|
||||
project = "search_api"
|
||||
datestamp = "1357740322"
|
||||
|
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Install, update and uninstall functions for the Search facets module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_uninstall().
|
||||
*/
|
||||
function search_api_facetapi_uninstall() {
|
||||
variable_del('search_api_facets_search_ids');
|
||||
}
|
@@ -0,0 +1,381 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Integrates the Search API with the Facet API.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_menu().
|
||||
*/
|
||||
function search_api_facetapi_menu() {
|
||||
// We need to handle our own menu paths for facets because we need a facet
|
||||
// configuration page per index.
|
||||
$first = TRUE;
|
||||
foreach (facetapi_get_realm_info() as $realm_name => $realm) {
|
||||
if ($first) {
|
||||
$first = FALSE;
|
||||
$items['admin/config/search/search_api/index/%search_api_index/facets'] = array(
|
||||
'title' => 'Facets',
|
||||
'page callback' => 'search_api_facetapi_settings',
|
||||
'page arguments' => array($realm_name, 5),
|
||||
'weight' => -1,
|
||||
'access arguments' => array('administer search_api'),
|
||||
'type' => MENU_LOCAL_TASK,
|
||||
'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
|
||||
);
|
||||
$items['admin/config/search/search_api/index/%search_api_index/facets/' . $realm_name] = array(
|
||||
'title' => $realm['label'],
|
||||
'type' => MENU_DEFAULT_LOCAL_TASK,
|
||||
'weight' => $realm['weight'],
|
||||
);
|
||||
}
|
||||
else {
|
||||
$items['admin/config/search/search_api/index/%search_api_index/facets/' . $realm_name] = array(
|
||||
'title' => $realm['label'],
|
||||
'page callback' => 'search_api_facetapi_settings',
|
||||
'page arguments' => array($realm_name, 5),
|
||||
'access arguments' => array('administer search_api'),
|
||||
'type' => MENU_LOCAL_TASK,
|
||||
'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
|
||||
'weight' => $realm['weight'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_facetapi_searcher_info().
|
||||
*/
|
||||
function search_api_facetapi_facetapi_searcher_info() {
|
||||
$info = array();
|
||||
$indexes = search_api_index_load_multiple(FALSE);
|
||||
foreach ($indexes as $index) {
|
||||
if ($index->enabled && $index->server()->supportsFeature('search_api_facets')) {
|
||||
$searcher_name = 'search_api@' . $index->machine_name;
|
||||
$info[$searcher_name] = array(
|
||||
'label' => t('Search service: @name', array('@name' => $index->name)),
|
||||
'adapter' => 'search_api',
|
||||
'instance' => $index->machine_name,
|
||||
'types' => array($index->item_type),
|
||||
'path' => '',
|
||||
'supports facet missing' => TRUE,
|
||||
'supports facet mincount' => TRUE,
|
||||
'include default facets' => FALSE,
|
||||
);
|
||||
}
|
||||
}
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_facetapi_facet_info().
|
||||
*/
|
||||
function search_api_facetapi_facetapi_facet_info(array $searcher_info) {
|
||||
$facet_info = array();
|
||||
if ('search_api' == $searcher_info['adapter']) {
|
||||
$index = search_api_index_load($searcher_info['instance']);
|
||||
if (!empty($index->options['fields'])) {
|
||||
$wrapper = $index->entityWrapper();
|
||||
$bundle_key = NULL;
|
||||
if (($entity_info = entity_get_info($index->item_type)) && !empty($entity_info['bundle keys']['bundle'])) {
|
||||
$bundle_key = $entity_info['bundle keys']['bundle'];
|
||||
}
|
||||
|
||||
// Some type-specific settings. Allowing to set some additional callbacks
|
||||
// (and other settings) in the map options allows for easier overriding by
|
||||
// other modules.
|
||||
$type_settings = array(
|
||||
'taxonomy_term' => array(
|
||||
'hierarchy callback' => 'facetapi_get_taxonomy_hierarchy',
|
||||
),
|
||||
'date' => array(
|
||||
'query type' => 'date',
|
||||
'map options' => array(
|
||||
'map callback' => 'facetapi_map_date',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Iterate through the indexed fields to set the facetapi settings for
|
||||
// each one.
|
||||
foreach ($index->getFields() as $key => $field) {
|
||||
$field['key'] = $key;
|
||||
// Determine which, if any, of the field type-specific options will be
|
||||
// used for this field.
|
||||
$type = isset($field['entity_type']) ? $field['entity_type'] : $field['type'];
|
||||
$type_settings += array($type => array());
|
||||
|
||||
$facet_info[$key] = $type_settings[$type] + array(
|
||||
'label' => $field['name'],
|
||||
'description' => t('Filter by @type.', array('@type' => $field['name'])),
|
||||
'allowed operators' => array(
|
||||
FACETAPI_OPERATOR_AND => TRUE,
|
||||
FACETAPI_OPERATOR_OR => $index->server()->supportsFeature('search_api_facets_operator_or'),
|
||||
),
|
||||
'dependency plugins' => array('role'),
|
||||
'facet missing allowed' => TRUE,
|
||||
'facet mincount allowed' => TRUE,
|
||||
'map callback' => 'search_api_facetapi_facet_map_callback',
|
||||
'map options' => array(),
|
||||
'field type' => $type,
|
||||
);
|
||||
if ($type === 'date') {
|
||||
$facet_info[$key]['description'] .= ' ' . t('(Caution: This may perform very poorly for large result sets.)');
|
||||
}
|
||||
$facet_info[$key]['map options'] += array(
|
||||
'field' => $field,
|
||||
'index id' => $index->machine_name,
|
||||
'value callback' => '_search_api_facetapi_facet_create_label',
|
||||
);
|
||||
// Find out whether this property is a Field API field.
|
||||
if (strpos($key, ':') === FALSE) {
|
||||
if (isset($wrapper->$key)) {
|
||||
$property_info = $wrapper->$key->info();
|
||||
if (!empty($property_info['field'])) {
|
||||
$facet_info[$key]['field api name'] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add bundle information, if applicable.
|
||||
if ($bundle_key) {
|
||||
if ($key === $bundle_key) {
|
||||
// Set entity type this field contains bundle information for.
|
||||
$facet_info[$key]['field api bundles'][] = $index->item_type;
|
||||
}
|
||||
else {
|
||||
// Add "bundle" as possible dependency plugin.
|
||||
$facet_info[$key]['dependency plugins'][] = 'bundle';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $facet_info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_facetapi_adapters().
|
||||
*/
|
||||
function search_api_facetapi_facetapi_adapters() {
|
||||
return array(
|
||||
'search_api' => array(
|
||||
'handler' => array(
|
||||
'class' => 'SearchApiFacetapiAdapter',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_facetapi_query_types().
|
||||
*/
|
||||
function search_api_facetapi_facetapi_query_types() {
|
||||
return array(
|
||||
'search_api_term' => array(
|
||||
'handler' => array(
|
||||
'class' => 'SearchApiFacetapiTerm',
|
||||
'adapter' => 'search_api',
|
||||
),
|
||||
),
|
||||
'search_api_date' => array(
|
||||
'handler' => array(
|
||||
'class' => 'SearchApiFacetapiDate',
|
||||
'adapter' => 'search_api',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_search_api_query_alter().
|
||||
*
|
||||
* Adds Facet API support to the query.
|
||||
*/
|
||||
function search_api_facetapi_search_api_query_alter($query) {
|
||||
$index = $query->getIndex();
|
||||
if ($index->server()->supportsFeature('search_api_facets')) {
|
||||
// This is the main point of communication between the facet system and the
|
||||
// search back-end - it makes the query respond to active facets.
|
||||
$searcher = 'search_api@' . $index->machine_name;
|
||||
$adapter = facetapi_adapter_load($searcher);
|
||||
if ($adapter) {
|
||||
$adapter->addActiveFilters($query);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu callback for the facet settings page.
|
||||
*/
|
||||
function search_api_facetapi_settings($realm_name, SearchApiIndex $index) {
|
||||
if (!$index->enabled) {
|
||||
return array('#markup' => t('Since this index is at the moment disabled, no facets can be activated.'));
|
||||
}
|
||||
if (!$index->server()->supportsFeature('search_api_facets')) {
|
||||
return array('#markup' => t('This index uses a server that does not support facet functionality.'));
|
||||
}
|
||||
$searcher_name = 'search_api@' . $index->machine_name;
|
||||
module_load_include('inc', 'facetapi', 'facetapi.admin');
|
||||
return drupal_get_form('facetapi_realm_settings_form', $searcher_name, $realm_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map callback for all search_api facet fields.
|
||||
*
|
||||
* @param array $values
|
||||
* The values to map.
|
||||
* @param array $options
|
||||
* An associative array containing:
|
||||
* - field: Field information, as stored in the index, but with an additional
|
||||
* "key" property set to the field's internal name.
|
||||
* - index id: The machine name of the index for this facet.
|
||||
* - map callback: (optional) A callback that will be called at the beginning,
|
||||
* which allows initial mapping of filters. Only values not mapped by that
|
||||
* callback will be processed by this method.
|
||||
* - value callback: A callback used to map single values and the limits of
|
||||
* ranges. The signature is the same as for this function, but all values
|
||||
* will be single values.
|
||||
* - missing label: (optional) The label used for the "missing" facet.
|
||||
*
|
||||
* @return array
|
||||
* An array mapping raw filter values to their labels.
|
||||
*/
|
||||
function search_api_facetapi_facet_map_callback(array $values, array $options = array()) {
|
||||
$map = array();
|
||||
// See if we have an additional map callback.
|
||||
if (isset($options['map callback']) && is_callable($options['map callback'])) {
|
||||
$map = call_user_func($options['map callback'], $values, $options);
|
||||
}
|
||||
|
||||
// Then look at all unmapped values and save information for them.
|
||||
$mappable_values = array();
|
||||
$ranges = array();
|
||||
foreach ($values as $value) {
|
||||
$value = (string) $value;
|
||||
if (isset($map[$value])) {
|
||||
continue;
|
||||
}
|
||||
if ($value == '!') {
|
||||
// The "missing" filter is usually always the same, but we allow an easy
|
||||
// override via the "missing label" map option.
|
||||
$map['!'] = isset($options['missing label']) ? $options['missing label'] : '(' . t('none') . ')';
|
||||
continue;
|
||||
}
|
||||
$length = strlen($value);
|
||||
if ($length > 5 && $value[0] == '[' && $value[$length - 1] == ']' && ($pos = strpos($value, ' TO '))) {
|
||||
// This is a range filter.
|
||||
$lower = trim(substr($value, 1, $pos));
|
||||
$upper = trim(substr($value, $pos + 4, -1));
|
||||
if ($lower != '*') {
|
||||
$mappable_values[$lower] = TRUE;
|
||||
}
|
||||
if ($upper != '*') {
|
||||
$mappable_values[$upper] = TRUE;
|
||||
}
|
||||
$ranges[$value] = array(
|
||||
'lower' => $lower,
|
||||
'upper' => $upper,
|
||||
);
|
||||
}
|
||||
else {
|
||||
// A normal, single-value filter.
|
||||
$mappable_values[$value] = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
if ($mappable_values) {
|
||||
$map += call_user_func($options['value callback'], array_keys($mappable_values), $options);
|
||||
}
|
||||
|
||||
foreach ($ranges as $value => $range) {
|
||||
$lower = isset($map[$range['lower']]) ? $map[$range['lower']] : $range['lower'];
|
||||
$upper = isset($map[$range['upper']]) ? $map[$range['upper']] : $range['upper'];
|
||||
if ($lower == '*' && $upper == '*') {
|
||||
$map[$value] = t('any');
|
||||
}
|
||||
elseif ($lower == '*') {
|
||||
$map[$value] = "< $upper";
|
||||
}
|
||||
elseif ($upper == '*') {
|
||||
$map[$value] = "> $lower";
|
||||
}
|
||||
else {
|
||||
$map[$value] = "$lower – $upper";
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a human-readable label for single facet filter values.
|
||||
*/
|
||||
function _search_api_facetapi_facet_create_label(array $values, array $options) {
|
||||
$field = $options['field'];
|
||||
// For entities, we can simply use the entity labels.
|
||||
if (isset($field['entity_type'])) {
|
||||
$type = $field['entity_type'];
|
||||
$entities = entity_load($type, $values);
|
||||
$map = array();
|
||||
foreach ($entities as $id => $entity) {
|
||||
$label = entity_label($type, $entity);
|
||||
if ($label) {
|
||||
$map[$id] = $label;
|
||||
}
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
// Then, we check whether there is an options list for the field.
|
||||
$index = search_api_index_load($options['index id']);
|
||||
$wrapper = $index->entityWrapper();
|
||||
foreach (explode(':', $field['key']) as $part) {
|
||||
if (!isset($wrapper->$part)) {
|
||||
$wrapper = NULL;
|
||||
break;
|
||||
}
|
||||
$wrapper = $wrapper->$part;
|
||||
while (($info = $wrapper->info()) && search_api_is_list_type($info['type'])) {
|
||||
$wrapper = $wrapper[0];
|
||||
}
|
||||
}
|
||||
if ($wrapper && ($options = $wrapper->optionsList('view'))) {
|
||||
return $options;
|
||||
}
|
||||
// As a "last resort" we try to create a label based on the field type.
|
||||
$map = array();
|
||||
foreach ($values as $value) {
|
||||
switch ($field['type']) {
|
||||
case 'boolean':
|
||||
$map[$value] = $value ? t('true') : t('false');
|
||||
break;
|
||||
case 'date':
|
||||
$v = is_numeric($value) ? $value : strtotime($value);
|
||||
$map[$value] = format_date($v, 'short');
|
||||
break;
|
||||
case 'duration':
|
||||
$map[$value] = format_interval($value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_form_FORM_ID_alter().
|
||||
*/
|
||||
function search_api_facetapi_form_search_api_admin_index_fields_alter(&$form, &$form_state) {
|
||||
$form['#submit'][] = 'search_api_facetapi_search_api_admin_index_fields_submit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for search_api_admin_index_fields().
|
||||
*/
|
||||
function search_api_facetapi_search_api_admin_index_fields_submit($form, &$form_state) {
|
||||
// Clears this searcher's cached facet definitions.
|
||||
$cid = 'facetapi:facet_info:search_api@' . $form_state['index']->machine_name . ':';
|
||||
cache_clear_all($cid, 'cache', TRUE);
|
||||
}
|
@@ -0,0 +1,71 @@
|
||||
Search API Views integration
|
||||
----------------------------
|
||||
|
||||
This module integrates the Search API with the popular Views module [1],
|
||||
allowing users to create views with filters, arguments, sorts and fields based
|
||||
on any search index.
|
||||
|
||||
[1] http://drupal.org/project/views
|
||||
|
||||
"More like this" feature
|
||||
------------------------
|
||||
This module defines the "More like this" feature (feature key: "search_api_mlt")
|
||||
that search service classes can implement. With a server supporting this, you
|
||||
can use the „More like this“ contextual filter to display a list of items
|
||||
related to a given item (usually, nodes similar to the node currently viewed).
|
||||
|
||||
For developers:
|
||||
A service class that wants to support this feature has to check for a
|
||||
"search_api_mlt" option in the search() method. When present, it will be an
|
||||
array containing two keys:
|
||||
- id: The entity ID of the item to which related items should be searched.
|
||||
- fields: An array of indexed fields to use for testing the similarity of items.
|
||||
When these are present, the normal keywords should be ignored and the related
|
||||
items be returned as results instead. Sorting, filtering and range restriction
|
||||
should all work normally.
|
||||
|
||||
"Facets block" display
|
||||
----------------------
|
||||
Most features should be clear to users of Views. However, the module also
|
||||
provides a new display type, "Facets block", that might need some explanation.
|
||||
This display type is only available, if the „Search facets“ module is also
|
||||
enabled.
|
||||
|
||||
The basic use of the block is to provide a list of links to the most popular
|
||||
filter terms (i.e., the ones with the most results) for a certain category. For
|
||||
example, you could provide a block listing the most popular authors, or taxonomy
|
||||
terms, linking to searches for those, to provide some kind of landing page.
|
||||
|
||||
Please note that, due to limitations in Views, this display mode is shown for
|
||||
views of all base tables, even though it only works for views based on Search
|
||||
API indexes. For views of other base tables, this will just print an error
|
||||
message.
|
||||
The display will also always ignore the view's "Style" setting, selected fields
|
||||
and sorts, etc.
|
||||
|
||||
To use the display, specify the base path of the search you want to link to
|
||||
(this enables you to also link to searches that aren't based on Views) and the
|
||||
facet field to use (any indexed field can be used here, there needn't be a facet
|
||||
defined for it). You'll then have the block available in the blocks
|
||||
administration and can enable and move it at leisure.
|
||||
Note, however, that the facet in question has to be enabled for the search page
|
||||
linked to for the filter to have an effect.
|
||||
|
||||
Since the block will trigger a search on pages where it is set to appear, you
|
||||
can also enable additional „normal“ facet blocks for that search, via the
|
||||
„Facets“ tab for the index. They will automatically also point to the same
|
||||
search that you specified for the display. The Search ID of the „Facets blocks“
|
||||
display can easily be recognized by the "-facet_block" suffix.
|
||||
If you want to use only the normal facets and not display anything at all in
|
||||
the Views block, just activate the display's „Hide block“ option.
|
||||
|
||||
Note: If you want to display the block not only on a few pages, you should in
|
||||
any case take care that it isn't displayed on the search page, since that might
|
||||
confuse users.
|
||||
|
||||
FAQ: Why „*Indexed* Node“?
|
||||
--------------------------
|
||||
The group name used for the search result itself (in fields, filters, etc.) is
|
||||
prefixed with „Indexed“ in order to be distinguishable from fields on referenced
|
||||
nodes (or other entities). The data displayed normally still comes from the
|
||||
entity, not from the search index.
|
@@ -0,0 +1,262 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Display plugin for displaying the search facets in a block.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Plugin class for displaying search facets in a block.
|
||||
*/
|
||||
class SearchApiViewsFacetsBlockDisplay extends views_plugin_display_block {
|
||||
public function displays_exposed() {
|
||||
return FALSE;
|
||||
}
|
||||
public function uses_exposed() {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
public function option_definition() {
|
||||
$options = parent::option_definition();
|
||||
|
||||
$options['linked_path'] = array('default' => '');
|
||||
$options['facet_field'] = '';
|
||||
$options['hide_block'] = FALSE;
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
public function options_form(&$form, &$form_state) {
|
||||
parent::options_form($form, $form_state);
|
||||
|
||||
if (substr($this->view->base_table, 0, 17) != 'search_api_index_') {
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($form_state['section']) {
|
||||
case 'linked_path':
|
||||
$form['#title'] .= t('Search page path');
|
||||
$form['linked_path'] = array(
|
||||
'#type' => 'textfield',
|
||||
'#description' => t('The menu path to which search facets will link. Leave empty to use the current path.'),
|
||||
'#default_value' => $this->get_option('linked_path'),
|
||||
);
|
||||
break;
|
||||
case 'facet_field':
|
||||
$form['facet_field'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Facet field'),
|
||||
'#options' => $this->getFieldOptions(),
|
||||
'#default_value' => $this->get_option('facet_field'),
|
||||
);
|
||||
break;
|
||||
case 'use_more':
|
||||
$form['use_more']['#description'] = t('This will add a more link to the bottom of this view, which will link to the base path for the facet links.');
|
||||
$form['use_more_always'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => $this->get_option('use_more_always'),
|
||||
);
|
||||
break;
|
||||
case 'hide_block':
|
||||
$form['hide_block'] = array(
|
||||
'#type' => 'checkbox',
|
||||
'#title' => t('Hide block'),
|
||||
'#description' => t('Hide this block, but still execute the search. ' .
|
||||
'Can be used to show native Facet API facet blocks linking to the search page specified above.'),
|
||||
'#default_value' => $this->get_option('hide_block'),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function options_validate(&$form, &$form_state) {
|
||||
if (substr($this->view->base_table, 0, 17) != 'search_api_index_') {
|
||||
form_set_error('', t('The "Facets block" display can only be used with base tables based on Search API indexes.'));
|
||||
}
|
||||
}
|
||||
|
||||
public function options_submit(&$form, &$form_state) {
|
||||
parent::options_submit($form, $form_state);
|
||||
|
||||
switch ($form_state['section']) {
|
||||
case 'linked_path':
|
||||
$this->set_option('linked_path', $form_state['values']['linked_path']);
|
||||
break;
|
||||
case 'facet_field':
|
||||
$this->set_option('facet_field', $form_state['values']['facet_field']);
|
||||
break;
|
||||
case 'hide_block':
|
||||
$this->set_option('hide_block', $form_state['values']['hide_block']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function options_summary(&$categories, &$options) {
|
||||
parent::options_summary($categories, $options);
|
||||
|
||||
$options['linked_path'] = array(
|
||||
'category' => 'block',
|
||||
'title' => t('Search page path'),
|
||||
'value' => $this->get_option('linked_path') ? $this->get_option('linked_path') : t('Use current path'),
|
||||
);
|
||||
$field_options = $this->getFieldOptions();
|
||||
$options['facet_field'] = array(
|
||||
'category' => 'block',
|
||||
'title' => t('Facet field'),
|
||||
'value' => $this->get_option('facet_field') ? $field_options[$this->get_option('facet_field')] : t('None'),
|
||||
);
|
||||
$options['hide_block'] = array(
|
||||
'category' => 'block',
|
||||
'title' => t('Hide block'),
|
||||
'value' => $this->get_option('hide_block') ? t('Yes') : t('No'),
|
||||
);
|
||||
}
|
||||
|
||||
protected $field_options = NULL;
|
||||
|
||||
protected function getFieldOptions() {
|
||||
if (!isset($this->field_options)) {
|
||||
$index_id = substr($this->view->base_table, 17);
|
||||
if (!($index_id && ($index = search_api_index_load($index_id)))) {
|
||||
$table = views_fetch_data($this->view->base_table);
|
||||
$table = empty($table['table']['base']['title']) ? $this->view->base_table : $table['table']['base']['title'];
|
||||
throw new SearchApiException(t('The "Facets block" display cannot be used with a view for @basetable. ' .
|
||||
'Please only use this display with base tables representing search indexes.',
|
||||
array('@basetable' => $table)));
|
||||
}
|
||||
$this->field_options = array();
|
||||
if (!empty($index->options['fields'])) {
|
||||
foreach ($index->getFields() as $key => $field) {
|
||||
$this->field_options[$key] = $field['name'];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $this->field_options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the 'more' link
|
||||
*/
|
||||
public function render_more_link() {
|
||||
if ($this->use_more()) {
|
||||
$path = $this->get_option('linked_path');
|
||||
$theme = views_theme_functions('views_more', $this->view, $this->display);
|
||||
$path = check_url(url($path, array()));
|
||||
|
||||
return array(
|
||||
'#theme' => $theme,
|
||||
'#more_url' => $path,
|
||||
'#link_text' => check_plain($this->use_more_text()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function execute() {
|
||||
if (substr($this->view->base_table, 0, 17) != 'search_api_index_') {
|
||||
form_set_error('', t('The "Facets block" display can only be used with base tables based on Search API indexes.'));
|
||||
return NULL;
|
||||
}
|
||||
$facet_field = $this->get_option('facet_field');
|
||||
if (!$facet_field) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$base_path = $this->get_option('linked_path');
|
||||
if (!$base_path) {
|
||||
$base_path = $_GET['q'];
|
||||
}
|
||||
$this->view->build();
|
||||
$limit = empty($this->view->query->pager->options['items_per_page']) ? 10 : $this->view->query->pager->options['items_per_page'];
|
||||
$query_options = &$this->view->query->getOptions();
|
||||
if (!$this->get_option('hide_block')) {
|
||||
// If we hide the block, we don't need this extra facet.
|
||||
$query_options['search_api_facets']['search_api_views_facets_block'] = array(
|
||||
'field' => $facet_field,
|
||||
'limit' => $limit,
|
||||
'missing' => FALSE,
|
||||
'min_count' => 1,
|
||||
);
|
||||
}
|
||||
$query_options['search id'] = 'search_api_views:' . $this->view->name . '-facets_block';
|
||||
$query_options['search_api_base_path'] = $base_path;
|
||||
$this->view->query->range(0, 0);
|
||||
|
||||
$this->view->execute();
|
||||
|
||||
if ($this->get_option('hide_block')) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$results = $this->view->query->getSearchApiResults();
|
||||
|
||||
if (empty($results['search_api_facets']['search_api_views_facets_block'])) {
|
||||
return NULL;
|
||||
}
|
||||
$terms = $results['search_api_facets']['search_api_views_facets_block'];
|
||||
|
||||
$filters = array();
|
||||
foreach ($terms as $term) {
|
||||
$filter = $term['filter'];
|
||||
if ($filter[0] == '"') {
|
||||
$filter = substr($filter, 1, -1);
|
||||
}
|
||||
elseif ($filter != '!') {
|
||||
// This is a range filter.
|
||||
$filter = substr($filter, 1, -1);
|
||||
$pos = strpos($filter, ' ');
|
||||
if ($pos !== FALSE) {
|
||||
$filter = '[' . substr($filter, 0, $pos) . ' TO ' . substr($filter, $pos + 1) . ']';
|
||||
}
|
||||
}
|
||||
$filters[$term['filter']] = $filter;
|
||||
}
|
||||
|
||||
$index = $this->view->query->getIndex();
|
||||
$options['field'] = $index->options['fields'][$facet_field];
|
||||
$options['field']['key'] = $facet_field;
|
||||
$options['index id'] = $index->machine_name;
|
||||
$options['value callback'] = '_search_api_facetapi_facet_create_label';
|
||||
$map = search_api_facetapi_facet_map_callback($filters, $options);
|
||||
|
||||
$facets = array();
|
||||
$prefix = rawurlencode($facet_field) . ':';
|
||||
foreach ($terms as $term) {
|
||||
$name = $filter = $filters[$term['filter']];
|
||||
if (isset($map[$filter])) {
|
||||
$name = $map[$filter];
|
||||
}
|
||||
$query['f'][0] = $prefix . $filter;
|
||||
|
||||
// Initializes variables passed to theme hook.
|
||||
$variables = array(
|
||||
'text' => $name,
|
||||
'path' => $base_path,
|
||||
'count' => $term['count'],
|
||||
'options' => array(
|
||||
'attributes' => array('class' => 'facetapi-inactive'),
|
||||
'html' => FALSE,
|
||||
'query' => $query,
|
||||
),
|
||||
);
|
||||
|
||||
// Themes the link, adds row to facets.
|
||||
$facets[] = array(
|
||||
'class' => array('leaf'),
|
||||
'data' => theme('facetapi_link_inactive', $variables),
|
||||
);
|
||||
}
|
||||
|
||||
if (!$facets) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$info['content']['facets'] = array(
|
||||
'#theme' => 'item_list',
|
||||
'#items' => $facets,
|
||||
);
|
||||
$info['content']['more'] = $this->render_more_link();
|
||||
$info['subject'] = filter_xss_admin($this->view->get_title());
|
||||
return $info;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Views argument handler class for handling all non-fulltext types.
|
||||
*/
|
||||
class SearchApiViewsHandlerArgument extends views_handler_argument {
|
||||
|
||||
/**
|
||||
* The associated views query object.
|
||||
*
|
||||
* @var SearchApiViewsQuery
|
||||
*/
|
||||
public $query;
|
||||
|
||||
/**
|
||||
* Determine if the argument can generate a breadcrumb
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
// @todo Change and implement set_breadcrumb()?
|
||||
public function uses_breadcrumb() {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a list of default behaviors for this argument if the argument
|
||||
* is not present.
|
||||
*
|
||||
* Override this method to provide additional (or fewer) default behaviors.
|
||||
*/
|
||||
public function default_actions($which = NULL) {
|
||||
$defaults = array(
|
||||
'ignore' => array(
|
||||
'title' => t('Display all values'),
|
||||
'method' => 'default_ignore',
|
||||
'breadcrumb' => TRUE, // generate a breadcrumb to here
|
||||
),
|
||||
'not found' => array(
|
||||
'title' => t('Hide view / Page not found (404)'),
|
||||
'method' => 'default_not_found',
|
||||
'hard fail' => TRUE, // This is a hard fail condition
|
||||
),
|
||||
'empty' => array(
|
||||
'title' => t('Display empty text'),
|
||||
'method' => 'default_empty',
|
||||
'breadcrumb' => TRUE, // generate a breadcrumb to here
|
||||
),
|
||||
'default' => array(
|
||||
'title' => t('Provide default argument'),
|
||||
'method' => 'default_default',
|
||||
'form method' => 'default_argument_form',
|
||||
'has default argument' => TRUE,
|
||||
'default only' => TRUE, // this can only be used for missing argument, not validation failure
|
||||
),
|
||||
);
|
||||
|
||||
if ($which) {
|
||||
return isset($defaults[$which]) ? $defaults[$which] : NULL;
|
||||
}
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
public function option_definition() {
|
||||
$options = parent::option_definition();
|
||||
|
||||
$options['break_phrase'] = array('default' => FALSE);
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
public function options_form(&$form, &$form_state) {
|
||||
parent::options_form($form, $form_state);
|
||||
|
||||
// Allow passing multiple values.
|
||||
$form['break_phrase'] = array(
|
||||
'#type' => 'checkbox',
|
||||
'#title' => t('Allow multiple values'),
|
||||
'#description' => t('If selected, users can enter multiple values in the form of 1+2+3 (for OR) or 1,2,3 (for AND).'),
|
||||
'#default_value' => $this->options['break_phrase'],
|
||||
'#fieldset' => 'more',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the query for this argument.
|
||||
*
|
||||
* The argument sent may be found at $this->argument.
|
||||
*/
|
||||
// @todo Provide options to select the operator, instead of always using '='?
|
||||
public function query($group_by = FALSE) {
|
||||
if (!empty($this->options['break_phrase'])) {
|
||||
views_break_phrase($this->argument, $this);
|
||||
}
|
||||
else {
|
||||
$this->value = array($this->argument);
|
||||
}
|
||||
|
||||
if (count($this->value) > 1) {
|
||||
$filter = $this->query->createFilter(drupal_strtoupper($this->operator));
|
||||
// $filter will be NULL if there were errors in the query.
|
||||
if ($filter) {
|
||||
foreach ($this->value as $value) {
|
||||
$filter->condition($this->real_field, $value, '=');
|
||||
}
|
||||
$this->query->filter($filter);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->query->condition($this->real_field, reset($this->value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the title this argument will assign the view, given the argument.
|
||||
*
|
||||
* This usually needs to be overridden to provide a proper title.
|
||||
*/
|
||||
public function title() {
|
||||
return t('Search @field for "@arg"', array('@field' => $this->definition['title'], '@arg' => $this->argument));
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Views argument handler class for handling fulltext fields.
|
||||
*/
|
||||
class SearchApiViewsHandlerArgumentFulltext extends SearchApiViewsHandlerArgumentText {
|
||||
|
||||
/**
|
||||
* Specify the options this filter uses.
|
||||
*/
|
||||
public function option_definition() {
|
||||
$options = parent::option_definition();
|
||||
$options['fields'] = array('default' => array());
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the options form a bit.
|
||||
*/
|
||||
public function options_form(&$form, &$form_state) {
|
||||
parent::options_form($form, $form_state);
|
||||
|
||||
$fields = $this->getFulltextFields();
|
||||
if (!empty($fields)) {
|
||||
$form['fields'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Searched fields'),
|
||||
'#description' => t('Select the fields that will be searched. If no fields are selected, all available fulltext fields will be searched.'),
|
||||
'#options' => $fields,
|
||||
'#size' => min(4, count($fields)),
|
||||
'#multiple' => TRUE,
|
||||
'#default_value' => $this->options['fields'],
|
||||
);
|
||||
}
|
||||
else {
|
||||
$form['fields'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => array(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the query for this argument.
|
||||
*
|
||||
* The argument sent may be found at $this->argument.
|
||||
*/
|
||||
public function query($group_by = FALSE) {
|
||||
if ($this->options['fields']) {
|
||||
$this->query->fields($this->options['fields']);
|
||||
}
|
||||
|
||||
$old = $this->query->getOriginalKeys();
|
||||
$this->query->keys($this->argument);
|
||||
if ($old) {
|
||||
$keys = &$this->query->getKeys();
|
||||
if (is_array($keys)) {
|
||||
$keys[] = $old;
|
||||
}
|
||||
elseif (is_array($old)) {
|
||||
// We don't support such nonsense.
|
||||
}
|
||||
else {
|
||||
$keys = "($old) ($keys)";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get an option list of all available fulltext fields.
|
||||
*/
|
||||
protected function getFulltextFields() {
|
||||
$ret = array();
|
||||
$index = search_api_index_load(substr($this->table, 17));
|
||||
if (!empty($index->options['fields'])) {
|
||||
$fields = $index->getFields();
|
||||
foreach ($index->getFulltextFields() as $field) {
|
||||
$ret[$field] = $fields[$field]['name'];
|
||||
}
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Views argument handler providing a list of related items for search servers
|
||||
* supporting the "search_api_mlt" feature.
|
||||
*/
|
||||
class SearchApiViewsHandlerArgumentMoreLikeThis extends SearchApiViewsHandlerArgument {
|
||||
|
||||
/**
|
||||
* Specify the options this filter uses.
|
||||
*/
|
||||
public function option_definition() {
|
||||
$options = parent::option_definition();
|
||||
unset($options['break_phrase']);
|
||||
$options['fields'] = array('default' => array());
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the options form a bit.
|
||||
*/
|
||||
public function options_form(&$form, &$form_state) {
|
||||
parent::options_form($form, $form_state);
|
||||
unset($form['break_phrase']);
|
||||
|
||||
$index = search_api_index_load(substr($this->table, 17));
|
||||
if (!empty($index->options['fields'])) {
|
||||
$fields = array();
|
||||
foreach ($index->getFields() as $key => $field) {
|
||||
$fields[$key] = $field['name'];
|
||||
}
|
||||
}
|
||||
if (!empty($fields)) {
|
||||
$form['fields'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Fields for Similarity'),
|
||||
'#description' => t('Select the fields that will be used for finding similar content. If no fields are selected, all available fields will be used.'),
|
||||
'#options' => $fields,
|
||||
'#size' => min(8, count($fields)),
|
||||
'#multiple' => TRUE,
|
||||
'#default_value' => $this->options['fields'],
|
||||
);
|
||||
}
|
||||
else {
|
||||
$form['fields'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => array(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the query for this argument.
|
||||
*
|
||||
* The argument sent may be found at $this->argument.
|
||||
*/
|
||||
public function query($group_by = FALSE) {
|
||||
$server = $this->query->getIndex()->server();
|
||||
if (!$server->supportsFeature('search_api_mlt')) {
|
||||
$class = search_api_get_service_info($server->class);
|
||||
throw new SearchApiException(t('The search service "@class" does not offer "More like this" functionality.',
|
||||
array('@class' => $class['name'])));
|
||||
return;
|
||||
}
|
||||
$fields = $this->options['fields'] ? $this->options['fields'] : array();
|
||||
if (empty($fields)) {
|
||||
foreach ($this->query->getIndex()->options['fields'] as $key => $field) {
|
||||
$fields[] = $key;
|
||||
}
|
||||
}
|
||||
$mlt = array(
|
||||
'id' => $this->argument,
|
||||
'fields' => $fields,
|
||||
);
|
||||
$this->query->getSearchApiQuery()->setOption('search_api_mlt', $mlt);
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Views argument handler class for handling fulltext fields.
|
||||
*/
|
||||
class SearchApiViewsHandlerArgumentText extends SearchApiViewsHandlerArgument {
|
||||
|
||||
/**
|
||||
* Get the title this argument will assign the view, given the argument.
|
||||
*
|
||||
* This usually needs to be overridden to provide a proper title.
|
||||
*/
|
||||
public function title() {
|
||||
return t('Search for "@arg"', array('@field' => $this->definition['title'], '@arg' => $this->argument));
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Views filter handler base class for handling all "normal" cases.
|
||||
*/
|
||||
class SearchApiViewsHandlerFilter extends views_handler_filter {
|
||||
|
||||
/**
|
||||
* The value to filter for.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
public $value;
|
||||
|
||||
/**
|
||||
* The operator used for filtering.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $operator;
|
||||
|
||||
/**
|
||||
* The associated views query object.
|
||||
*
|
||||
* @var SearchApiViewsQuery
|
||||
*/
|
||||
public $query;
|
||||
|
||||
/**
|
||||
* Provide a list of options for the operator form.
|
||||
*/
|
||||
public function operator_options() {
|
||||
return array(
|
||||
'<' => t('Is smaller than'),
|
||||
'<=' => t('Is smaller than or equal to'),
|
||||
'=' => t('Is equal to'),
|
||||
'<>' => t('Is not equal to'),
|
||||
'>=' => t('Is greater than or equal to'),
|
||||
'>' => t('Is greater than'),
|
||||
'empty' => t('Is empty'),
|
||||
'not empty' => t('Is not empty'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a form for setting the filter value.
|
||||
*/
|
||||
public function value_form(&$form, &$form_state) {
|
||||
while (is_array($this->value)) {
|
||||
$this->value = $this->value ? array_shift($this->value) : NULL;
|
||||
}
|
||||
$form['value'] = array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => empty($form_state['exposed']) ? t('Value') : '',
|
||||
'#size' => 30,
|
||||
'#default_value' => isset($this->value) ? $this->value : '',
|
||||
);
|
||||
|
||||
// Hide the value box if the operator is 'empty' or 'not empty'.
|
||||
// Radios share the same selector so we have to add some dummy selector.
|
||||
$form['value']['#states']['visible'] = array(
|
||||
':input[name="options[operator]"],dummy-empty' => array('!value' => 'empty'),
|
||||
':input[name="options[operator]"],dummy-not-empty' => array('!value' => 'not empty'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the filter on the administrative summary
|
||||
*/
|
||||
function admin_summary() {
|
||||
if (!empty($this->options['exposed'])) {
|
||||
return t('exposed');
|
||||
}
|
||||
|
||||
if ($this->operator === 'empty') {
|
||||
return t('is empty');
|
||||
}
|
||||
if ($this->operator === 'not empty') {
|
||||
return t('is not empty');
|
||||
}
|
||||
|
||||
return check_plain((string) $this->operator) . ' ' . check_plain((string) $this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add this filter to the query.
|
||||
*/
|
||||
public function query() {
|
||||
if ($this->operator === 'empty') {
|
||||
$this->query->condition($this->real_field, NULL, '=', $this->options['group']);
|
||||
}
|
||||
elseif ($this->operator === 'not empty') {
|
||||
$this->query->condition($this->real_field, NULL, '<>', $this->options['group']);
|
||||
}
|
||||
else {
|
||||
while (is_array($this->value)) {
|
||||
$this->value = $this->value ? reset($this->value) : NULL;
|
||||
}
|
||||
if (strlen($this->value) > 0) {
|
||||
$this->query->condition($this->real_field, $this->value, $this->operator, $this->options['group']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Views filter handler class for handling fulltext fields.
|
||||
*/
|
||||
class SearchApiViewsHandlerFilterBoolean extends SearchApiViewsHandlerFilter {
|
||||
|
||||
/**
|
||||
* Provide a list of options for the operator form.
|
||||
*/
|
||||
public function operator_options() {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a form for setting the filter value.
|
||||
*/
|
||||
public function value_form(&$form, &$form_state) {
|
||||
while (is_array($this->value)) {
|
||||
$this->value = $this->value ? array_shift($this->value) : NULL;
|
||||
}
|
||||
$form['value'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => empty($form_state['exposed']) ? t('Value') : '',
|
||||
'#options' => array(1 => t('True'), 0 => t('False')),
|
||||
'#default_value' => isset($this->value) ? $this->value : '',
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Views filter handler base class for handling all "normal" cases.
|
||||
*/
|
||||
class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilter {
|
||||
|
||||
/**
|
||||
* Add a "widget type" option.
|
||||
*/
|
||||
public function option_definition() {
|
||||
return parent::option_definition() + array(
|
||||
'widget_type' => array('default' => 'default'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the date popup module is enabled, provide the extra option setting.
|
||||
*/
|
||||
public function has_extra_options() {
|
||||
if (module_exists('date_popup')) {
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add extra options if we allow the date popup widget.
|
||||
*/
|
||||
public function extra_options_form(&$form, &$form_state) {
|
||||
parent::extra_options_form($form, $form_state);
|
||||
if (module_exists('date_popup')) {
|
||||
$widget_options = array('default' => 'Default', 'date_popup' => 'Date popup');
|
||||
$form['widget_type'] = array(
|
||||
'#type' => 'radios',
|
||||
'#title' => t('Date selection form element'),
|
||||
'#default_value' => $this->options['widget_type'],
|
||||
'#options' => $widget_options,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a form for setting the filter value.
|
||||
*/
|
||||
public function value_form(&$form, &$form_state) {
|
||||
parent::value_form($form, $form_state);
|
||||
|
||||
// If we are using the date popup widget, overwrite the settings of the form
|
||||
// according to what date_popup expects.
|
||||
if ($this->options['widget_type'] == 'date_popup' && module_exists('date_popup')) {
|
||||
$form['value']['#type'] = 'date_popup';
|
||||
$form['value']['#date_format'] = 'm/d/Y';
|
||||
unset($form['value']['#description']);
|
||||
}
|
||||
elseif (empty($form_state['exposed'])) {
|
||||
$form['value']['#description'] = t('A date in any format understood by <a href="@doc-link">PHP</a>. For example, "@date1" or "@date2".', array(
|
||||
'@doc-link' => 'http://php.net/manual/en/function.strtotime.php',
|
||||
'@date1' => format_date(REQUEST_TIME, 'custom', 'Y-m-d H:i:s'),
|
||||
'@date2' => 'now + 1 day',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add this filter to the query.
|
||||
*/
|
||||
public function query() {
|
||||
if ($this->operator === 'empty') {
|
||||
$this->query->condition($this->real_field, NULL, '=', $this->options['group']);
|
||||
}
|
||||
elseif ($this->operator === 'not empty') {
|
||||
$this->query->condition($this->real_field, NULL, '<>', $this->options['group']);
|
||||
}
|
||||
else {
|
||||
while (is_array($this->value)) {
|
||||
$this->value = $this->value ? reset($this->value) : NULL;
|
||||
}
|
||||
$v = is_numeric($this->value) ? $this->value : strtotime($this->value, REQUEST_TIME);
|
||||
if ($v !== FALSE) {
|
||||
$this->query->condition($this->real_field, $v, $this->operator, $this->options['group']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Views filter handler class for handling fulltext fields.
|
||||
*/
|
||||
class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterText {
|
||||
|
||||
/**
|
||||
* Specify the options this filter uses.
|
||||
*/
|
||||
public function option_definition() {
|
||||
$options = parent::option_definition();
|
||||
|
||||
$options['mode'] = array('default' => 'keys');
|
||||
$options['fields'] = array('default' => array());
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the options form a bit.
|
||||
*/
|
||||
public function options_form(&$form, &$form_state) {
|
||||
parent::options_form($form, $form_state);
|
||||
|
||||
$form['mode'] = array(
|
||||
'#title' => t('Use as'),
|
||||
'#type' => 'radios',
|
||||
'#options' => array(
|
||||
'keys' => t('Search keys – multiple words will be split and the filter will influence relevance.'),
|
||||
'filter' => t("Search filter – use as a single phrase that restricts the result set but doesn't influence relevance."),
|
||||
),
|
||||
'#default_value' => $this->options['mode'],
|
||||
);
|
||||
|
||||
$fields = $this->getFulltextFields();
|
||||
if (!empty($fields)) {
|
||||
$form['fields'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Searched fields'),
|
||||
'#description' => t('Select the fields that will be searched. If no fields are selected, all available fulltext fields will be searched.'),
|
||||
'#options' => $fields,
|
||||
'#size' => min(4, count($fields)),
|
||||
'#multiple' => TRUE,
|
||||
'#default_value' => $this->options['fields'],
|
||||
);
|
||||
}
|
||||
else {
|
||||
$form['fields'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => array(),
|
||||
);
|
||||
}
|
||||
if (isset($form['expose'])) {
|
||||
$form['expose']['#weight'] = -5;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add this filter to the query.
|
||||
*/
|
||||
public function query() {
|
||||
while (is_array($this->value)) {
|
||||
$this->value = $this->value ? reset($this->value) : '';
|
||||
}
|
||||
// Catch empty strings entered by the user, but not "0".
|
||||
if ($this->value === '') {
|
||||
return;
|
||||
}
|
||||
$fields = $this->options['fields'];
|
||||
$fields = $fields ? $fields : array_keys($this->getFulltextFields());
|
||||
|
||||
// If something already specifically set different fields, we silently fall
|
||||
// back to mere filtering.
|
||||
$filter = $this->options['mode'] == 'filter';
|
||||
if (!$filter) {
|
||||
$old = $this->query->getFields();
|
||||
$filter = $old && (array_diff($old, $fields) || array_diff($fields, $old));
|
||||
}
|
||||
|
||||
if ($filter) {
|
||||
$filter = $this->query->createFilter('OR');
|
||||
foreach ($fields as $field) {
|
||||
$filter->condition($field, $this->value, $this->operator);
|
||||
}
|
||||
$this->query->filter($filter);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->query->fields($fields);
|
||||
$old = $this->query->getOriginalKeys();
|
||||
$this->query->keys($this->value);
|
||||
if ($this->operator != '=') {
|
||||
$keys = &$this->query->getKeys();
|
||||
if (is_array($keys)) {
|
||||
$keys['#negation'] = TRUE;
|
||||
}
|
||||
else {
|
||||
// We can't know how negation is expressed in the server's syntax.
|
||||
}
|
||||
}
|
||||
if ($old) {
|
||||
$keys = &$this->query->getKeys();
|
||||
if (is_array($keys)) {
|
||||
$keys[] = $old;
|
||||
}
|
||||
elseif (is_array($old)) {
|
||||
// We don't support such nonsense.
|
||||
}
|
||||
else {
|
||||
$keys = "($old) ($keys)";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get an option list of all available fulltext fields.
|
||||
*/
|
||||
protected function getFulltextFields() {
|
||||
$fields = array();
|
||||
$index = search_api_index_load(substr($this->table, 17));
|
||||
if (!empty($index->options['fields'])) {
|
||||
$f = $index->getFields();
|
||||
foreach ($index->getFulltextFields() as $name) {
|
||||
$fields[$name] = $f[$name]['name'];
|
||||
}
|
||||
}
|
||||
return $fields;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains the SearchApiViewsHandlerFilterLanguage class.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Views filter handler class for handling the special "Item language" field.
|
||||
*
|
||||
* Definition items:
|
||||
* - options: An array of possible values for this field.
|
||||
*/
|
||||
class SearchApiViewsHandlerFilterLanguage extends SearchApiViewsHandlerFilterOptions {
|
||||
|
||||
/**
|
||||
* Provide a form for setting options.
|
||||
*/
|
||||
public function value_form(&$form, &$form_state) {
|
||||
parent::value_form($form, $form_state);
|
||||
$form['value']['#options'] = array(
|
||||
'current' => t("Current user's language"),
|
||||
'default' => t('Default site language'),
|
||||
) + $form['value']['#options'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a summary of this filter's value for the admin UI.
|
||||
*/
|
||||
public function admin_summary() {
|
||||
$tmp = $this->definition['options'];
|
||||
$this->definition['options']['current'] = t('current');
|
||||
$this->definition['options']['default'] = t('default');
|
||||
$ret = parent::admin_summary();
|
||||
$this->definition['options'] = $tmp;
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add this filter to the query.
|
||||
*/
|
||||
public function query() {
|
||||
global $language_content;
|
||||
foreach ($this->value as $i => $v) {
|
||||
if ($v == 'current') {
|
||||
$this->value[$i] = $language_content->language;
|
||||
}
|
||||
elseif ($v == 'default') {
|
||||
$this->value[$i] = language_default('language');
|
||||
}
|
||||
}
|
||||
parent::query();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Views filter handler class for handling fields with a limited set of possible
|
||||
* values.
|
||||
*
|
||||
* Definition items:
|
||||
* - options: An array of possible values for this field.
|
||||
*/
|
||||
class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter {
|
||||
|
||||
protected $value_form_type = 'checkboxes';
|
||||
|
||||
/**
|
||||
* Provide a list of options for the operator form.
|
||||
*/
|
||||
public function operator_options() {
|
||||
return array(
|
||||
'=' => t('Is one of'),
|
||||
'<>' => t('Is not one of'),
|
||||
'empty' => t('Is empty'),
|
||||
'not empty' => t('Is not empty'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set "reduce" option to FALSE by default.
|
||||
*/
|
||||
public function expose_options() {
|
||||
parent::expose_options();
|
||||
$this->options['expose']['reduce'] = FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the "reduce" option to the exposed form.
|
||||
*/
|
||||
public function expose_form(&$form, &$form_state) {
|
||||
parent::expose_form($form, $form_state);
|
||||
$form['expose']['reduce'] = array(
|
||||
'#type' => 'checkbox',
|
||||
'#title' => t('Limit list to selected items'),
|
||||
'#description' => t('If checked, the only items presented to the user will be the ones selected here.'),
|
||||
'#default_value' => !empty($this->options['expose']['reduce']),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define "reduce" option.
|
||||
*/
|
||||
public function option_definition() {
|
||||
$options = parent::option_definition();
|
||||
$options['expose']['contains']['reduce'] = array('default' => FALSE);
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce the options according to the selection.
|
||||
*/
|
||||
protected function reduce_value_options() {
|
||||
$options = array();
|
||||
foreach ($this->definition['options'] as $id => $option) {
|
||||
if (isset($this->options['value'][$id])) {
|
||||
$options[$id] = $option;
|
||||
}
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save set checkboxes.
|
||||
*/
|
||||
public function value_submit($form, &$form_state) {
|
||||
// Drupal's FAPI system automatically puts '0' in for any checkbox that
|
||||
// was not set, and the key to the checkbox if it is set.
|
||||
// Unfortunately, this means that if the key to that checkbox is 0,
|
||||
// we are unable to tell if that checkbox was set or not.
|
||||
|
||||
// Luckily, the '#value' on the checkboxes form actually contains
|
||||
// *only* a list of checkboxes that were set, and we can use that
|
||||
// instead.
|
||||
|
||||
$form_state['values']['options']['value'] = $form['value']['#value'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a form for setting options.
|
||||
*/
|
||||
public function value_form(&$form, &$form_state) {
|
||||
$options = array();
|
||||
if (!empty($this->options['expose']['reduce']) && !empty($form_state['exposed'])) {
|
||||
$options += $this->reduce_value_options($form_state);
|
||||
}
|
||||
else {
|
||||
$options += $this->definition['options'];
|
||||
}
|
||||
$form['value'] = array(
|
||||
'#type' => $this->value_form_type,
|
||||
'#title' => empty($form_state['exposed']) ? t('Value') : '',
|
||||
'#options' => $options,
|
||||
'#multiple' => TRUE,
|
||||
'#size' => min(4, count($this->definition['options'])),
|
||||
'#default_value' => isset($this->value) ? $this->value : array(),
|
||||
);
|
||||
|
||||
// Hide the value box if operator is 'empty' or 'not empty'.
|
||||
// Radios share the same selector so we have to add some dummy selector.
|
||||
// #states replace #dependency (http://drupal.org/node/1595022).
|
||||
$form['value']['#states']['visible'] = array(
|
||||
':input[name="options[operator]"],dummy-empty' => array('!value' => 'empty'),
|
||||
':input[name="options[operator]"],dummy-not-empty' => array('!value' => 'not empty'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a summary of this filter's value for the admin UI.
|
||||
*/
|
||||
public function admin_summary() {
|
||||
if (!empty($this->options['exposed'])) {
|
||||
return t('exposed');
|
||||
}
|
||||
|
||||
if ($this->operator === 'empty') {
|
||||
return t('is empty');
|
||||
}
|
||||
if ($this->operator === 'not empty') {
|
||||
return t('is not empty');
|
||||
}
|
||||
|
||||
if (!is_array($this->value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$operator_options = $this->operator_options();
|
||||
$operator = $operator_options[$this->operator];
|
||||
$values = '';
|
||||
|
||||
// Remove every element which is not known.
|
||||
foreach ($this->value as $i => $value) {
|
||||
if (!isset($this->definition['options'][$value])) {
|
||||
unset($this->value[$i]);
|
||||
}
|
||||
}
|
||||
// Choose different kind of ouput for 0, a single and multiple values.
|
||||
if (count($this->value) == 0) {
|
||||
return $this->operator == '=' ? t('none') : t('any');
|
||||
}
|
||||
elseif (count($this->value) == 1) {
|
||||
// If there is only a single value, use just the plain operator, = or <>.
|
||||
$operator = check_plain($this->operator);
|
||||
$values = check_plain($this->definition['options'][reset($this->value)]);
|
||||
}
|
||||
else {
|
||||
foreach ($this->value as $value) {
|
||||
if ($values !== '') {
|
||||
$values .= ', ';
|
||||
}
|
||||
if (drupal_strlen($values) > 20) {
|
||||
$values .= '…';
|
||||
break;
|
||||
}
|
||||
$values .= check_plain($this->definition['options'][$value]);
|
||||
}
|
||||
}
|
||||
|
||||
return $operator . (($values !== '') ? ' ' . $values : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add this filter to the query.
|
||||
*/
|
||||
public function query() {
|
||||
if ($this->operator === 'empty') {
|
||||
$this->query->condition($this->real_field, NULL, '=', $this->options['group']);
|
||||
}
|
||||
elseif ($this->operator === 'not empty') {
|
||||
$this->query->condition($this->real_field, NULL, '<>', $this->options['group']);
|
||||
}
|
||||
else {
|
||||
while (is_array($this->value) && count($this->value) == 1) {
|
||||
$this->value = reset($this->value);
|
||||
}
|
||||
if (is_scalar($this->value) && $this->value !== '') {
|
||||
$this->query->condition($this->real_field, $this->value, $this->operator, $this->options['group']);
|
||||
}
|
||||
elseif ($this->value) {
|
||||
if ($this->operator == '=') {
|
||||
$filter = $this->query->createFilter('OR');
|
||||
// $filter will be NULL if there were errors in the query.
|
||||
if ($filter) {
|
||||
foreach ($this->value as $v) {
|
||||
$filter->condition($this->real_field, $v, '=');
|
||||
}
|
||||
$this->query->filter($filter, $this->options['group']);
|
||||
}
|
||||
}
|
||||
else {
|
||||
foreach ($this->value as $v) {
|
||||
$this->query->condition($this->real_field, $v, $this->operator, $this->options['group']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Views filter handler class for handling fulltext fields.
|
||||
*/
|
||||
class SearchApiViewsHandlerFilterText extends SearchApiViewsHandlerFilter {
|
||||
|
||||
/**
|
||||
* Provide a list of options for the operator form.
|
||||
*/
|
||||
public function operator_options() {
|
||||
return array('=' => t('contains'), '<>' => t("doesn't contain"));
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class for sorting results according to a specified field.
|
||||
*/
|
||||
class SearchApiViewsHandlerSort extends views_handler_sort {
|
||||
|
||||
/**
|
||||
* The associated views query object.
|
||||
*
|
||||
* @var SearchApiViewsQuery
|
||||
*/
|
||||
public $query;
|
||||
|
||||
/**
|
||||
* Called to add the sort to a query.
|
||||
*/
|
||||
public function query() {
|
||||
// When there are exposed sorts, the "exposed form" plugin will set
|
||||
// $query->orderby to an empty array. Therefore, if that property is set,
|
||||
// we here remove all previous sorts.
|
||||
if (isset($this->query->orderby)) {
|
||||
unset($this->query->orderby);
|
||||
$sort = &$this->query->getSort();
|
||||
$sort = array();
|
||||
}
|
||||
$this->query->sort($this->real_field, $this->options['order']);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,564 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Views query class using a Search API index as the data source.
|
||||
*/
|
||||
class SearchApiViewsQuery extends views_plugin_query {
|
||||
|
||||
/**
|
||||
* Number of results to display.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $limit;
|
||||
|
||||
/**
|
||||
* Offset of first displayed result.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $offset;
|
||||
|
||||
/**
|
||||
* The index this view accesses.
|
||||
*
|
||||
* @var SearchApiIndex
|
||||
*/
|
||||
protected $index;
|
||||
|
||||
/**
|
||||
* The query that will be executed.
|
||||
*
|
||||
* @var SearchApiQueryInterface
|
||||
*/
|
||||
protected $query;
|
||||
|
||||
/**
|
||||
* The results returned by the query, after it was executed.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $search_api_results = array();
|
||||
|
||||
/**
|
||||
* Array of all encountered errors.
|
||||
*
|
||||
* Each of these is fatal, meaning that a non-empty $errors property will
|
||||
* result in an empty result being returned.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $errors;
|
||||
|
||||
/**
|
||||
* The names of all fields whose value is required by a handler.
|
||||
*
|
||||
* The format follows the same as Search API field identifiers (parent:child).
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fields;
|
||||
|
||||
/**
|
||||
* The query's sub-filters representing the different Views filter groups.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $filters = array();
|
||||
|
||||
/**
|
||||
* The conjunction with which multiple filter groups are combined.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $group_operator = 'AND';
|
||||
|
||||
/**
|
||||
* Create the basic query object and fill with default values.
|
||||
*/
|
||||
public function init($base_table, $base_field, $options) {
|
||||
try {
|
||||
$this->errors = array();
|
||||
parent::init($base_table, $base_field, $options);
|
||||
$this->fields = array();
|
||||
if (substr($base_table, 0, 17) == 'search_api_index_') {
|
||||
$id = substr($base_table, 17);
|
||||
$this->index = search_api_index_load($id);
|
||||
$this->query = $this->index->query(array(
|
||||
'parse mode' => 'terms',
|
||||
));
|
||||
}
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$this->errors[] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a field that should be retrieved from the results by this view.
|
||||
*
|
||||
* @param $field
|
||||
* The field's identifier, as used by the Search API. E.g., "title" for a
|
||||
* node's title, "author:name" for a node's author's name.
|
||||
*
|
||||
* @return SearchApiViewsQuery
|
||||
* The called object.
|
||||
*/
|
||||
public function addField($field) {
|
||||
$this->fields[$field] = TRUE;
|
||||
return $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a sort to the query.
|
||||
*
|
||||
* @param $selector
|
||||
* The field to sort on. All indexed fields of the index are valid values.
|
||||
* In addition, the special fields 'search_api_relevance' (sort by
|
||||
* relevance) and 'search_api_id' (sort by item id) may be used.
|
||||
* @param $order
|
||||
* The order to sort items in - either 'ASC' or 'DESC'. Defaults to 'ASC'.
|
||||
*/
|
||||
public function add_selector_orderby($selector, $order = 'ASC') {
|
||||
$this->query->sort($selector, $order);
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the options used by this query plugin.
|
||||
*
|
||||
* Adds an option to bypass access checks.
|
||||
*/
|
||||
public function option_definition() {
|
||||
return parent::option_definition() + array(
|
||||
'search_api_bypass_access' => array(
|
||||
'default' => FALSE,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add settings for the UI.
|
||||
*
|
||||
* Adds an option for bypassing access checks.
|
||||
*/
|
||||
public function options_form(&$form, &$form_state) {
|
||||
parent::options_form($form, $form_state);
|
||||
|
||||
$form['search_api_bypass_access'] = array(
|
||||
'#type' => 'checkbox',
|
||||
'#title' => t('Bypass access checks'),
|
||||
'#description' => t('If the underlying search index has access checks enabled, this option allows to disable them for this view.'),
|
||||
'#default_value' => $this->options['search_api_bypass_access'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the necessary info to execute the query.
|
||||
*/
|
||||
public function build(&$view) {
|
||||
$this->view = $view;
|
||||
|
||||
// Setup the nested filter structure for this query.
|
||||
if (!empty($this->where)) {
|
||||
// If the different groups are combined with the OR operator, we have to
|
||||
// add a new OR filter to the query to which the filters for the groups
|
||||
// will be added.
|
||||
if ($this->group_operator === 'OR') {
|
||||
$base = $this->query->createFilter('OR');
|
||||
$this->query->filter($base);
|
||||
}
|
||||
else {
|
||||
$base = $this->query;
|
||||
}
|
||||
// Add a nested filter for each filter group, with its set conjunction.
|
||||
foreach ($this->where as $group_id => $group) {
|
||||
if (!empty($group['conditions']) || !empty($group['filters'])) {
|
||||
// For filters without a group, we want to always add them directly to
|
||||
// the query.
|
||||
$filter = ($group_id === '') ? $this->query : $this->query->createFilter($group['type']);
|
||||
if (!empty($group['conditions'])) {
|
||||
foreach ($group['conditions'] as $condition) {
|
||||
list($field, $value, $operator) = $condition;
|
||||
$filter->condition($field, $value, $operator);
|
||||
}
|
||||
}
|
||||
if (!empty($group['filters'])) {
|
||||
foreach ($group['filters'] as $nested_filter) {
|
||||
$filter->filter($nested_filter);
|
||||
}
|
||||
}
|
||||
// If no group was given, the filters were already set on the query.
|
||||
if ($group_id !== '') {
|
||||
$base->filter($filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the pager and let it modify the query to add limits.
|
||||
$view->init_pager();
|
||||
$this->pager->query();
|
||||
|
||||
// Add the "search_api_bypass_access" option to the query, if desired.
|
||||
if (!empty($this->options['search_api_bypass_access'])) {
|
||||
$this->query->setOption('search_api_bypass_access', TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the query and fills the associated view object with according
|
||||
* values.
|
||||
*
|
||||
* Values to set: $view->result, $view->total_rows, $view->execute_time,
|
||||
* $view->pager['current_page'].
|
||||
*/
|
||||
public function execute(&$view) {
|
||||
if ($this->errors) {
|
||||
if (error_displayable()) {
|
||||
foreach ($this->errors as $msg) {
|
||||
drupal_set_message(check_plain($msg), 'error');
|
||||
}
|
||||
}
|
||||
$view->result = array();
|
||||
$view->total_rows = 0;
|
||||
$view->execute_time = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$start = microtime(TRUE);
|
||||
// Add range and search ID (if it wasn't already set).
|
||||
$this->query->range($this->offset, $this->limit);
|
||||
if ($this->query->getOption('search id') == get_class($this->query)) {
|
||||
$this->query->setOption('search id', 'search_api_views:' . $view->name . ':' . $view->current_display);
|
||||
}
|
||||
|
||||
// Execute the search.
|
||||
$results = $this->query->execute();
|
||||
$this->search_api_results = $results;
|
||||
|
||||
// Store the results.
|
||||
$this->pager->total_items = $view->total_rows = $results['result count'];
|
||||
if (!empty($this->pager->options['offset'])) {
|
||||
$this->pager->total_items -= $this->pager->options['offset'];
|
||||
}
|
||||
$this->pager->update_page_info();
|
||||
$view->result = array();
|
||||
if (!empty($results['results'])) {
|
||||
$this->addResults($results['results'], $view);
|
||||
}
|
||||
// We shouldn't use $results['performance']['complete'] here, since
|
||||
// extracting the results probably takes considerable time as well.
|
||||
$view->execute_time = microtime(TRUE) - $start;
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$this->errors[] = $e->getMessage();
|
||||
// Recursion to get the same error behaviour as above.
|
||||
return $this->execute($view);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for adding results to a view in the format expected by the
|
||||
* view.
|
||||
*/
|
||||
protected function addResults(array $results, $view) {
|
||||
$rows = array();
|
||||
$missing = array();
|
||||
$items = array();
|
||||
|
||||
// First off, we try to gather as much field values as possible without
|
||||
// loading any items.
|
||||
foreach ($results as $id => $result) {
|
||||
$row = array();
|
||||
|
||||
// Include the loaded item for this result row, if present, or the item
|
||||
// ID.
|
||||
if (!empty($result['entity'])) {
|
||||
$row['entity'] = $result['entity'];
|
||||
}
|
||||
else {
|
||||
$row['entity'] = $id;
|
||||
}
|
||||
|
||||
$row['_entity_properties']['search_api_relevance'] = $result['score'];
|
||||
$row['_entity_properties']['search_api_excerpt'] = empty($result['excerpt']) ? '' : $result['excerpt'];
|
||||
|
||||
// Gather any fields from the search results.
|
||||
if (!empty($result['fields'])) {
|
||||
$row['_entity_properties'] += $result['fields'];
|
||||
}
|
||||
|
||||
// Check whether we need to extract any properties from the result item.
|
||||
$missing_fields = array_diff_key($this->fields, $row);
|
||||
if ($missing_fields) {
|
||||
$missing[$id] = $missing_fields;
|
||||
if (is_object($row['entity'])) {
|
||||
$items[$id] = $row['entity'];
|
||||
}
|
||||
else {
|
||||
$ids[] = $id;
|
||||
}
|
||||
}
|
||||
|
||||
// Save the row values for adding them to the Views result afterwards.
|
||||
$rows[$id] = (object) $row;
|
||||
}
|
||||
|
||||
// Load items of those rows which haven't got all field values, yet.
|
||||
if (!empty($ids)) {
|
||||
$items += $this->index->loadItems($ids);
|
||||
// $items now includes loaded items, and those already passed in the
|
||||
// search results.
|
||||
foreach ($items as $id => $item) {
|
||||
// Extract item properties.
|
||||
$wrapper = $this->index->entityWrapper($item, FALSE);
|
||||
$rows[$id]->_entity_properties += $this->extractFields($wrapper, $missing[$id]);
|
||||
$rows[$id]->entity = $item;
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, add all rows to the Views result set.
|
||||
$view->result = array_values($rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for extracting all necessary fields from a result item.
|
||||
*
|
||||
* Usually, this method isn't needed anymore as the properties are now
|
||||
* extracted by the field handlers themselves.
|
||||
*/
|
||||
protected function extractFields(EntityMetadataWrapper $wrapper, array $all_fields) {
|
||||
$fields = array();
|
||||
foreach ($all_fields as $key => $true) {
|
||||
$fields[$key]['type'] = 'string';
|
||||
}
|
||||
$fields = search_api_extract_fields($wrapper, $fields, array('sanitized' => TRUE));
|
||||
$ret = array();
|
||||
foreach ($all_fields as $key => $true) {
|
||||
$ret[$key] = isset($fields[$key]['value']) ? $fields[$key]['value'] : '';
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the according entity objects for the given query results.
|
||||
*
|
||||
* This is necessary to support generic entity handlers and plugins with this
|
||||
* query backend.
|
||||
*
|
||||
* If the current query isn't based on an entity type, the method will return
|
||||
* an empty array.
|
||||
*/
|
||||
public function get_result_entities($results, $relationship = NULL, $field = NULL) {
|
||||
list($type, $wrappers) = $this->get_result_wrappers($results, $relationship, $field);
|
||||
$return = array();
|
||||
foreach ($wrappers as $id => $wrapper) {
|
||||
try {
|
||||
$return[$id] = $wrapper->value();
|
||||
}
|
||||
catch (EntityMetadataWrapperException $e) {
|
||||
// Ignore.
|
||||
}
|
||||
}
|
||||
return array($type, $return);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the according metadata wrappers for the given query results.
|
||||
*
|
||||
* This is necessary to support generic entity handlers and plugins with this
|
||||
* query backend.
|
||||
*/
|
||||
public function get_result_wrappers($results, $relationship = NULL, $field = NULL) {
|
||||
$is_entity = (boolean) entity_get_info($this->index->item_type);
|
||||
$wrappers = array();
|
||||
$load_entities = array();
|
||||
foreach ($results as $row_index => $row) {
|
||||
if ($is_entity && isset($row->entity)) {
|
||||
// If this entity isn't load, register it for pre-loading.
|
||||
if (!is_object($row->entity)) {
|
||||
$load_entities[$row->entity] = $row_index;
|
||||
}
|
||||
|
||||
$wrappers[$row_index] = $this->index->entityWrapper($row->entity);
|
||||
}
|
||||
}
|
||||
|
||||
// If the results are entities, we pre-load them to make use of a multiple
|
||||
// load. (Otherwise, each result would be loaded individually.)
|
||||
if (!empty($load_entities)) {
|
||||
$entities = entity_load($this->index->item_type, array_keys($load_entities));
|
||||
foreach ($entities as $entity_id => $entity) {
|
||||
$wrappers[$load_entities[$entity_id]] = $this->index->entityWrapper($entity);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the relationship, if necessary.
|
||||
$type = $this->index->item_type;
|
||||
$selector_suffix = '';
|
||||
if ($field && ($pos = strrpos($field, ':'))) {
|
||||
$selector_suffix = substr($field, 0, $pos);
|
||||
}
|
||||
if ($selector_suffix || ($relationship && !empty($this->view->relationship[$relationship]))) {
|
||||
// Use EntityFieldHandlerHelper to compute the correct data selector for
|
||||
// the relationship.
|
||||
$handler = (object) array(
|
||||
'view' => $this->view,
|
||||
'relationship' => $relationship,
|
||||
'real_field' => '',
|
||||
);
|
||||
$selector = EntityFieldHandlerHelper::construct_property_selector($handler);
|
||||
$selector .= ($selector ? ':' : '') . $selector_suffix;
|
||||
list($type, $wrappers) = EntityFieldHandlerHelper::extract_property_multiple($wrappers, $selector);
|
||||
}
|
||||
|
||||
return array($type, $wrappers);
|
||||
}
|
||||
|
||||
/**
|
||||
* API function for accessing the raw Search API query object.
|
||||
*
|
||||
* @return SearchApiQueryInterface
|
||||
* The search query object used internally by this handler.
|
||||
*/
|
||||
public function getSearchApiQuery() {
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
/**
|
||||
* API function for accessing the raw Search API results.
|
||||
*
|
||||
* @return array
|
||||
* An associative array containing the search results, as specified by
|
||||
* SearchApiQueryInterface::execute().
|
||||
*/
|
||||
public function getSearchApiResults() {
|
||||
return $this->search_api_results;
|
||||
}
|
||||
|
||||
//
|
||||
// Query interface methods (proxy to $this->query)
|
||||
//
|
||||
|
||||
public function createFilter($conjunction = 'AND') {
|
||||
if (!$this->errors) {
|
||||
return $this->query->createFilter($conjunction);
|
||||
}
|
||||
}
|
||||
|
||||
public function keys($keys = NULL) {
|
||||
if (!$this->errors) {
|
||||
$this->query->keys($keys);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function fields(array $fields) {
|
||||
if (!$this->errors) {
|
||||
$this->query->fields($fields);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a nested filter to the search query object.
|
||||
*
|
||||
* If $group is given, the filter is added to the relevant filter group
|
||||
* instead.
|
||||
*/
|
||||
public function filter(SearchApiQueryFilterInterface $filter, $group = NULL) {
|
||||
if (!$this->errors) {
|
||||
$this->where[$group]['filters'][] = $filter;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a condition on the search query object.
|
||||
*
|
||||
* If $group is given, the condition is added to the relevant filter group
|
||||
* instead.
|
||||
*/
|
||||
public function condition($field, $value, $operator = '=', $group = NULL) {
|
||||
if (!$this->errors) {
|
||||
$this->where[$group]['conditions'][] = array($field, $value, $operator);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function sort($field, $order = 'ASC') {
|
||||
if (!$this->errors) {
|
||||
$this->query->sort($field, $order);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function range($offset = NULL, $limit = NULL) {
|
||||
if (!$this->errors) {
|
||||
$this->query->range($offset, $limit);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIndex() {
|
||||
return $this->index;
|
||||
}
|
||||
|
||||
public function &getKeys() {
|
||||
if (!$this->errors) {
|
||||
return $this->query->getKeys();
|
||||
}
|
||||
$ret = NULL;
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function getOriginalKeys() {
|
||||
if (!$this->errors) {
|
||||
return $this->query->getOriginalKeys();
|
||||
}
|
||||
}
|
||||
|
||||
public function &getFields() {
|
||||
if (!$this->errors) {
|
||||
return $this->query->getFields();
|
||||
}
|
||||
$ret = NULL;
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function getFilter() {
|
||||
if (!$this->errors) {
|
||||
return $this->query->getFilter();
|
||||
}
|
||||
}
|
||||
|
||||
public function &getSort() {
|
||||
if (!$this->errors) {
|
||||
return $this->query->getSort();
|
||||
}
|
||||
$ret = NULL;
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function getOption($name) {
|
||||
if (!$this->errors) {
|
||||
return $this->query->getOption($name);
|
||||
}
|
||||
}
|
||||
|
||||
public function setOption($name, $value) {
|
||||
if (!$this->errors) {
|
||||
return $this->query->setOption($name, $value);
|
||||
}
|
||||
}
|
||||
|
||||
public function &getOptions() {
|
||||
if (!$this->errors) {
|
||||
return $this->query->getOptions();
|
||||
}
|
||||
$ret = NULL;
|
||||
return $ret;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
|
||||
name = Search views
|
||||
description = Integrates the Search API with Views, enabling users to create views with searches as filters or arguments.
|
||||
dependencies[] = search_api
|
||||
dependencies[] = views
|
||||
core = 7.x
|
||||
package = Search
|
||||
|
||||
; Views handlers
|
||||
files[] = includes/display_facet_block.inc
|
||||
files[] = includes/handler_argument.inc
|
||||
files[] = includes/handler_argument_fulltext.inc
|
||||
files[] = includes/handler_argument_more_like_this.inc
|
||||
files[] = includes/handler_argument_text.inc
|
||||
files[] = includes/handler_filter.inc
|
||||
files[] = includes/handler_filter_boolean.inc
|
||||
files[] = includes/handler_filter_date.inc
|
||||
files[] = includes/handler_filter_fulltext.inc
|
||||
files[] = includes/handler_filter_language.inc
|
||||
files[] = includes/handler_filter_options.inc
|
||||
files[] = includes/handler_filter_text.inc
|
||||
files[] = includes/handler_sort.inc
|
||||
files[] = includes/query.inc
|
||||
|
||||
; Information added by drupal.org packaging script on 2013-01-09
|
||||
version = "7.x-1.4+0-dev"
|
||||
core = "7.x"
|
||||
project = "search_api"
|
||||
datestamp = "1357740322"
|
||||
|
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
/**
|
||||
* @file
|
||||
* Install, update and uninstall functions for the search_api_views module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Updates all Search API views to use the new, specification-compliant identifiers.
|
||||
*/
|
||||
function search_api_views_update_7101() {
|
||||
$tables = views_fetch_data();
|
||||
// Contains arrays with real fields mapped to field IDs for each table.
|
||||
$table_fields = array();
|
||||
foreach ($tables as $key => $table) {
|
||||
if (substr($key, 0, 17) != 'search_api_index_') {
|
||||
continue;
|
||||
}
|
||||
foreach ($table as $field => $info) {
|
||||
if (isset($info['real field']) && $field != $info['real field']) {
|
||||
$table_fields[$key][$info['real field']] = $field;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$table_fields) {
|
||||
return;
|
||||
}
|
||||
foreach (views_get_all_views() as $name => $view) {
|
||||
if (empty($view->base_table) || empty($table_fields[$view->base_table])) {
|
||||
continue;
|
||||
}
|
||||
$change = FALSE;
|
||||
$fields = $table_fields[$view->base_table];
|
||||
$change |= _search_api_views_update_7101_helper($view->base_field, $fields);
|
||||
if (!empty($view->display)) {
|
||||
foreach ($view->display as $key => &$display) {
|
||||
$options = &$display->display_options;
|
||||
if (isset($options['style_options']['grouping'])) {
|
||||
$change |= _search_api_views_update_7101_helper($options['style_options']['grouping'], $fields);
|
||||
}
|
||||
if (isset($options['style_options']['columns'])) {
|
||||
$change |= _search_api_views_update_7101_helper($options['style_options']['columns'], $fields);
|
||||
}
|
||||
if (isset($options['style_options']['info'])) {
|
||||
$change |= _search_api_views_update_7101_helper($options['style_options']['info'], $fields);
|
||||
}
|
||||
if (isset($options['arguments'])) {
|
||||
$change |= _search_api_views_update_7101_helper($options['arguments'], $fields);
|
||||
}
|
||||
if (isset($options['fields'])) {
|
||||
$change |= _search_api_views_update_7101_helper($options['fields'], $fields);
|
||||
}
|
||||
if (isset($options['filters'])) {
|
||||
$change |= _search_api_views_update_7101_helper($options['filters'], $fields);
|
||||
}
|
||||
if (isset($options['sorts'])) {
|
||||
$change |= _search_api_views_update_7101_helper($options['sorts'], $fields);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($change) {
|
||||
$view->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for replacing field identifiers.
|
||||
*
|
||||
* @return
|
||||
* TRUE iff the identifier was changed.
|
||||
*/
|
||||
function _search_api_views_update_7101_helper(&$field, array $fields) {
|
||||
if (is_array($field)) {
|
||||
$change = FALSE;
|
||||
$new_field = array();
|
||||
foreach ($field as $k => $v) {
|
||||
$new_k = $k;
|
||||
$change |= _search_api_views_update_7101_helper($new_k, $fields);
|
||||
$change |= _search_api_views_update_7101_helper($v, $fields);
|
||||
$new_field[$new_k] = $v;
|
||||
}
|
||||
$field = $new_field;
|
||||
return $change;
|
||||
}
|
||||
if (isset($fields[$field])) {
|
||||
$field = $fields[$field];
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the now unnecessary "search_api_views_max_fields_depth" variable.
|
||||
*/
|
||||
function search_api_views_update_7102() {
|
||||
variable_del('search_api_views_max_fields_depth');
|
||||
}
|
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Implements hook_views_api().
|
||||
*/
|
||||
function search_api_views_views_api() {
|
||||
return array(
|
||||
'api' => '3.0-alpha1',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_search_api_index_insert().
|
||||
*/
|
||||
function search_api_views_search_api_index_insert(SearchApiIndex $index) {
|
||||
// Make the new index available for views.
|
||||
views_invalidate_cache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_search_api_index_update().
|
||||
*/
|
||||
function search_api_views_search_api_index_update(SearchApiIndex $index) {
|
||||
if (!$index->enabled && $index->original->enabled) {
|
||||
_search_api_views_index_unavailable($index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_search_api_index_delete().
|
||||
*/
|
||||
function search_api_views_search_api_index_delete(SearchApiIndex $index) {
|
||||
_search_api_views_index_unavailable($index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function for reacting to a disabled or deleted search index.
|
||||
*/
|
||||
function _search_api_views_index_unavailable(SearchApiIndex $index) {
|
||||
$names = array();
|
||||
$table = 'search_api_index_' . $index->machine_name;
|
||||
foreach (views_get_all_views() as $name => $view) {
|
||||
if (empty($view->disabled) && $view->base_table == $table) {
|
||||
$names[] = $name;
|
||||
// @todo: if ($index_deleted) $view->delete()?
|
||||
}
|
||||
}
|
||||
if ($names) {
|
||||
views_invalidate_cache();
|
||||
drupal_set_message(t('The following views were using the index %name: @views. You should disable or delete them.', array('%name' => $index->name, '@views' => implode(', ', $names))), 'warning');
|
||||
}
|
||||
}
|
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Implements hook_views_data().
|
||||
*/
|
||||
function search_api_views_views_data() {
|
||||
try {
|
||||
$data = array();
|
||||
$entity_types = entity_get_info();
|
||||
foreach (search_api_index_load_multiple(FALSE) as $index) {
|
||||
// Fill in base data.
|
||||
$key = 'search_api_index_' . $index->machine_name;
|
||||
$table = &$data[$key];
|
||||
$type_info = search_api_get_item_type_info($index->item_type);
|
||||
$table['table']['group'] = t('Indexed @entity_type', array('@entity_type' => $type_info['name']));
|
||||
$table['table']['base'] = array(
|
||||
'field' => 'search_api_id',
|
||||
'index' => $index->machine_name,
|
||||
'title' => $index->name,
|
||||
'help' => t('Use the %name search index for filtering and retrieving data.', array('%name' => $index->name)),
|
||||
'query class' => 'search_api_views_query',
|
||||
);
|
||||
if (isset($entity_types[$index->item_type])) {
|
||||
$table['table'] += array(
|
||||
'entity type' => $index->item_type,
|
||||
'skip entity load' => TRUE,
|
||||
);
|
||||
}
|
||||
|
||||
$wrapper = $index->entityWrapper(NULL, TRUE);
|
||||
|
||||
// Add field handlers and relationships provided by the Entity API.
|
||||
foreach ($wrapper as $key => $property) {
|
||||
$info = $property->info();
|
||||
if ($info) {
|
||||
entity_views_field_definition($key, $info, $table);
|
||||
}
|
||||
}
|
||||
|
||||
// Add handlers for all indexed fields.
|
||||
foreach ($index->getFields() as $key => $field) {
|
||||
$tmp = $wrapper;
|
||||
$group = '';
|
||||
$name = '';
|
||||
$parts = explode(':', $key);
|
||||
foreach ($parts as $i => $part) {
|
||||
if (!isset($tmp->$part)) {
|
||||
continue 2;
|
||||
}
|
||||
$tmp = $tmp->$part;
|
||||
$info = $tmp->info();
|
||||
$group = ($group ? $group . ' » ' . $name : ($name ? $name : ''));
|
||||
$name = $info['label'];
|
||||
if ($i < count($parts) - 1) {
|
||||
// Unwrap lists.
|
||||
$level = search_api_list_nesting_level($info['type']);
|
||||
for ($j = 0; $j < $level; ++$j) {
|
||||
$tmp = $tmp[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
$id = _entity_views_field_identifier($key, $table);
|
||||
if ($group) {
|
||||
// @todo Entity type label instead of $group?
|
||||
$table[$id]['group'] = $group;
|
||||
$name = t('@field (indexed)', array('@field' => $name));
|
||||
}
|
||||
$table[$id]['title'] = $name;
|
||||
$table[$id]['help'] = empty($info['description']) ? t('(No information available)') : $info['description'];
|
||||
$table[$id]['type'] = $field['type'];
|
||||
if ($id != $key) {
|
||||
$table[$id]['real field'] = $key;
|
||||
}
|
||||
_search_api_views_add_handlers($key, $field, $tmp, $table);
|
||||
}
|
||||
|
||||
// Special handlers
|
||||
$table['search_api_language']['filter']['handler'] = 'SearchApiViewsHandlerFilterLanguage';
|
||||
|
||||
$table['search_api_id']['title'] = t('Entity ID');
|
||||
$table['search_api_id']['help'] = t("The entity's ID.");
|
||||
$table['search_api_id']['sort']['handler'] = 'SearchApiViewsHandlerSort';
|
||||
|
||||
$table['search_api_relevance']['group'] = t('Search');
|
||||
$table['search_api_relevance']['title'] = t('Relevance');
|
||||
$table['search_api_relevance']['help'] = t('The relevance of this search result with respect to the query.');
|
||||
$table['search_api_relevance']['field']['type'] = 'decimal';
|
||||
$table['search_api_relevance']['field']['handler'] = 'entity_views_handler_field_numeric';
|
||||
$table['search_api_relevance']['field']['click sortable'] = TRUE;
|
||||
$table['search_api_relevance']['sort']['handler'] = 'SearchApiViewsHandlerSort';
|
||||
|
||||
$table['search_api_excerpt']['group'] = t('Search');
|
||||
$table['search_api_excerpt']['title'] = t('Excerpt');
|
||||
$table['search_api_excerpt']['help'] = t('The search result excerpted to show found search terms.');
|
||||
$table['search_api_excerpt']['field']['type'] = 'text';
|
||||
$table['search_api_excerpt']['field']['handler'] = 'entity_views_handler_field_text';
|
||||
|
||||
$table['search_api_views_fulltext']['group'] = t('Search');
|
||||
$table['search_api_views_fulltext']['title'] = t('Fulltext search');
|
||||
$table['search_api_views_fulltext']['help'] = t('Search several or all fulltext fields at once.');
|
||||
$table['search_api_views_fulltext']['filter']['handler'] = 'SearchApiViewsHandlerFilterFulltext';
|
||||
$table['search_api_views_fulltext']['argument']['handler'] = 'SearchApiViewsHandlerArgumentFulltext';
|
||||
|
||||
$table['search_api_views_more_like_this']['group'] = t('Search');
|
||||
$table['search_api_views_more_like_this']['title'] = t('More like this');
|
||||
$table['search_api_views_more_like_this']['help'] = t('Find similar content.');
|
||||
$table['search_api_views_more_like_this']['argument']['handler'] = 'SearchApiViewsHandlerArgumentMoreLikeThis';
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
catch (Exception $e) {
|
||||
watchdog_exception('search_api_views', $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function that returns an array of handler definitions to add to a
|
||||
* views field definition.
|
||||
*/
|
||||
function _search_api_views_add_handlers($id, array $field, EntityMetadataWrapper $wrapper, array &$table) {
|
||||
$type = $field['type'];
|
||||
$inner_type = search_api_extract_inner_type($type);
|
||||
|
||||
if (strpos($id, ':')) {
|
||||
entity_views_field_definition($id, $wrapper->info(), $table);
|
||||
}
|
||||
$id = _entity_views_field_identifier($id, $table);
|
||||
$table += array($id => array());
|
||||
|
||||
if ($inner_type == 'text') {
|
||||
$table[$id] += array(
|
||||
'argument' => array(
|
||||
'handler' => 'SearchApiViewsHandlerArgumentText',
|
||||
),
|
||||
'filter' => array(
|
||||
'handler' => 'SearchApiViewsHandlerFilterText',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($options = $wrapper->optionsList('view')) {
|
||||
$table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterOptions';
|
||||
$table[$id]['filter']['options'] = $options;
|
||||
}
|
||||
elseif ($inner_type == 'boolean') {
|
||||
$table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterBoolean';
|
||||
}
|
||||
elseif ($inner_type == 'date') {
|
||||
$table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterDate';
|
||||
}
|
||||
else {
|
||||
$table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilter';
|
||||
}
|
||||
|
||||
$table[$id]['argument']['handler'] = 'SearchApiViewsHandlerArgument';
|
||||
|
||||
// We can only sort according to single-valued fields.
|
||||
if ($type == $inner_type) {
|
||||
$table[$id]['sort']['handler'] = 'SearchApiViewsHandlerSort';
|
||||
if (isset($table[$id]['field'])) {
|
||||
$table[$id]['field']['click sortable'] = TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_views_plugins().
|
||||
*/
|
||||
function search_api_views_views_plugins() {
|
||||
$ret = array(
|
||||
'query' => array(
|
||||
'search_api_views_query' => array(
|
||||
'title' => t('Search API Query'),
|
||||
'help' => t('Query will be generated and run using the Search API.'),
|
||||
'handler' => 'SearchApiViewsQuery'
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (module_exists('search_api_facetapi')) {
|
||||
$ret['display']['search_api_views_facets_block'] = array(
|
||||
'title' => t('Facets block'),
|
||||
'help' => t('Display facets for this search as a block anywhere on the site.'),
|
||||
'handler' => 'SearchApiViewsFacetsBlockDisplay',
|
||||
'uses hook block' => TRUE,
|
||||
'use ajax' => FALSE,
|
||||
'use pager' => FALSE,
|
||||
'use more' => TRUE,
|
||||
'accept attachments' => TRUE,
|
||||
'admin' => t('Facets block'),
|
||||
);
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
Reference in New Issue
Block a user