Elgg  Version master
SearchService.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg\Search;
4 
6 use Elgg\Config;
7 use Elgg\Database;
15 
23 
25 
33  public function __construct(
34  protected Config $config,
35  protected EventsService $events,
36  protected Database $db
37  ) {
38  }
39 
61  public function search(array $options = []) {
63 
64  $query_parts = elgg_extract('query_parts', $options);
65  $fields = (array) elgg_extract('fields', $options);
66 
67  if (empty($query_parts) || empty(array_filter($fields))) {
68  return false;
69  }
70 
71  $entity_type = elgg_extract('type', $options, 'all', false);
72  $entity_subtype = elgg_extract('subtype', $options);
73  $search_type = elgg_extract('search_type', $options, 'entities');
74 
75  if ($entity_type !== 'all' && !in_array($entity_type, Config::ENTITY_TYPES)) {
76  throw new DomainException("'{$entity_type}' is not a valid entity type");
77  }
78 
79  $options = $this->events->triggerResults('search:options', $entity_type, $options, $options);
80  if (!empty($entity_subtype) && is_string($entity_subtype)) {
81  $options = $this->events->triggerResults('search:options', "{$entity_type}:{$entity_subtype}", $options, $options);
82  }
83 
84  $options = $this->events->triggerResults('search:options', $search_type, $options, $options);
85 
86  if ($this->events->hasHandler('search:results', $search_type)) {
87  $results = $this->events->triggerResults('search:results', $search_type, $options);
88  if (isset($results)) {
89  // allow events to conditionally replace the result set
90  return $results;
91  }
92  }
93 
95  }
96 
104  public function normalizeOptions(array $options = []) {
105 
106  if (elgg_extract('_elgg_search_service_normalize_options', $options)) {
107  // already normalized once before
108  return $options;
109  }
110 
111  $search_type = elgg_extract('search_type', $options, 'entities', false);
112  $options['search_type'] = $search_type;
113 
114  $options = $this->events->triggerResults('search:params', $search_type, $options, $options);
115 
116  $options = $this->normalizeQuery($options);
118 
119  // prevent duplicate normalization
120  $options['_elgg_search_service_normalize_options'] = true;
121 
122  return $options;
123  }
124 
132  public function prepareSearchOptions(array $options = []) {
134 
135  $fields = elgg_extract('fields', $options);
136  $query_parts = elgg_extract('query_parts', $options);
137  $partial = elgg_extract('partial_match', $options, true);
138 
139  $options['wheres']['search'] = function (QueryBuilder $qb, $alias) use ($fields, $query_parts, $partial) {
140  return $this->buildSearchWhereQuery($qb, $alias, $fields, $query_parts, $partial);
141  };
142 
143  return $options;
144  }
145 
153  public function normalizeQuery(array $options = []) {
154 
155  $query = elgg_extract('query', $options, '');
156  $query = strip_tags($query);
157  $query = htmlspecialchars($query, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8');
158  $query = trim($query);
159 
160  $words = preg_split('/\s+/', $query);
161  $words = array_map(function ($e) {
162  return trim($e);
163  }, $words);
164 
165  $query = implode(' ', $words);
166 
167  $options['query'] = $query;
168 
169  $tokenize = elgg_extract('tokenize', $options, true);
170  if ($tokenize) {
171  $parts = $words;
172  } else {
173  $parts = [$query];
174  }
175 
176  $options['query_parts'] = array_unique(array_filter($parts));
177 
178  return $options;
179  }
180 
188  public function normalizeSearchFields(array $options = []) {
189 
190  $default_fields = [
191  'attributes' => [],
192  'metadata' => [],
193  'annotations' => [],
194  ];
195 
196  $fields = $default_fields;
197 
198  $clean_field_property_types = function ($new_fields) use ($default_fields) {
199  $property_types = array_keys($default_fields);
200  foreach ($property_types as $property_type) {
201  if (empty($new_fields[$property_type])) {
202  $new_fields[$property_type] = [];
203  } else {
204  $new_fields[$property_type] = array_unique($new_fields[$property_type]);
205  }
206  }
207 
208  return $new_fields;
209  };
210 
211  $merge_fields = function ($new_fields) use (&$fields, $clean_field_property_types) {
212  if (empty($new_fields) || !is_array($new_fields)) {
213  return;
214  }
215 
216  $new_fields = $clean_field_property_types($new_fields);
217 
218  $fields = array_merge_recursive($fields, $new_fields);
219  };
220 
221  // normalize type/subtype to support all combinations
222  $normalized_options = $this->normalizeTypeSubtypeOptions($options);
223 
224  $type_subtype_pairs = elgg_extract('type_subtype_pairs', $normalized_options);
225  if (!empty($type_subtype_pairs)) {
226  foreach ($type_subtype_pairs as $entity_type => $entity_subtypes) {
227  $result = $this->events->triggerResults('search:fields', $entity_type, $options, $default_fields);
228  $merge_fields($result);
229 
230  if (elgg_is_empty($entity_subtypes)) {
231  continue;
232  }
233 
234  foreach ($entity_subtypes as $entity_subtype) {
235  $result = $this->events->triggerResults('search:fields', "{$entity_type}:{$entity_subtype}", $options, $default_fields);
236  $merge_fields($result);
237  }
238  }
239  }
240 
241  // search fields for search type
242  $search_type = elgg_extract('search_type', $options, 'entities');
243  if ($search_type) {
244  $fields = $this->events->triggerResults('search:fields', $search_type, $options, $fields);
245  }
246 
247  // make sure all supported field types are available
248  $fields = $clean_field_property_types($fields);
249 
250  if (empty($options['fields'])) {
251  $options['fields'] = $fields;
252  } else {
253  // only allow known fields
254  foreach ($fields as $property_type => $property_type_fields) {
255  if (empty($options['fields'][$property_type])) {
256  $options['fields'][$property_type] = [];
257  continue;
258  }
259 
260  $allowed = array_intersect($property_type_fields, (array) $options['fields'][$property_type]);
261  $options['fields'][$property_type] = array_values(array_unique($allowed));
262  }
263  }
264 
265  return $options;
266  }
267 
279  public function buildSearchWhereQuery(QueryBuilder $qb, $alias, $fields, $query_parts, $partial_match = true) {
280 
281  $attributes = elgg_extract('attributes', $fields, [], false);
282  $metadata = elgg_extract('metadata', $fields, [], false);
283  $annotations = elgg_extract('annotations', $fields, [], false);
284 
285  $ors = [];
286 
287  $populate_where = function ($where, $part) use ($partial_match) {
288  $where->values = $partial_match ? "%{$part}%" : $part;
289  $where->comparison = 'LIKE';
290  $where->value_type = ELGG_VALUE_STRING;
291  $where->case_sensitive = false;
292  };
293 
294  if (!empty($attributes)) {
295  foreach ($attributes as $attribute) {
296  $attribute_ands = [];
297  foreach ($query_parts as $part) {
298  $where = new AttributeWhereClause();
299  $where->names = $attribute;
300  $populate_where($where, $part);
301  $attribute_ands[] = $where->prepare($qb, $alias);
302  }
303 
304  $ors[] = $qb->merge($attribute_ands, 'AND');
305  }
306  }
307 
308  if (!empty($metadata)) {
309  $metadata_ands = [];
310  $md_alias = $qb->joinMetadataTable($alias, 'guid', $metadata, 'left');
311  foreach ($query_parts as $part) {
312  $where = new MetadataWhereClause();
313  $populate_where($where, $part);
314  $metadata_ands[] = $where->prepare($qb, $md_alias);
315  }
316 
317  $ors[] = $qb->merge($metadata_ands, 'AND');
318  }
319 
320  if (!empty($annotations)) {
321  $annotations_ands = [];
322  $an_alias = $qb->joinAnnotationTable($alias, 'guid', $annotations, 'left');
323  foreach ($query_parts as $part) {
324  $where = new AnnotationWhereClause();
325  $populate_where($where, $part);
326  $annotations_ands[] = $where->prepare($qb, $an_alias);
327  }
328 
329  $ors[] = $qb->merge($annotations_ands, 'AND');
330  }
331 
332  return $qb->merge($ors, 'OR');
333  }
334 }
joinAnnotationTable(string $from_alias= '', string $from_column= 'guid', $name=null,?string $join_type= 'inner', string $joined_alias=null)
Join annotations table from alias and return joined table alias.
normalizeOptions(array $options=[])
Normalize options.
__construct(protected Config $config, protected EventsService $events, protected Database $db)
Constructor.
const ENTITY_TYPES
Definition: Config.php:263
The Elgg database.
Definition: Database.php:25
Builds quereis for matching entities by their attributes.
Exception thrown if a value does not adhere to a defined valid data domain.
Events service.
Database abstraction query builder.
Builds queries for matching annotations against their properties.
normalizeQuery(array $options=[])
Normalize query parts.
elgg_is_empty($value)
Check if a value isn&#39;t empty, but allow 0 and &#39;0&#39;.
Definition: input.php:176
prepareSearchOptions(array $options=[])
Prepare ege* options.
elgg_extract($key, $array, $default=null, bool $strict=true)
Checks for $array[$key] and returns its value if it exists, else returns $default.
Definition: elgglib.php:254
$config
Advanced site settings, debugging section.
Definition: debugging.php:6
if($who_can_change_language=== 'nobody') elseif($who_can_change_language=== 'admin_only'&&!elgg_is_admin_logged_in()) $options
Definition: language.php:20
elgg_get_entities(array $options=[])
Fetches/counts entities or performs a calculation on their properties.
Definition: entities.php:507
$fields
Save the configuration of the security.txt contents.
Definition: security_txt.php:6
foreach($recommendedExtensions as $extension) if(empty(ini_get('session.gc_probability'))||empty(ini_get('session.gc_divisor'))) $db
search(array $options=[])
Returns search results as an array of entities, as a batch, or a count, depending on parameters given...
$results
Definition: content.php:22
joinMetadataTable(string $from_alias= '', string $from_column= 'guid', $name=null,?string $join_type= 'inner', string $joined_alias=null)
Join metadata table from alias and return joined table alias.
merge($parts=null, $boolean= 'AND')
Merges multiple composite expressions with a boolean.
const ELGG_VALUE_STRING
Definition: constants.php:112
$metadata
Output annotation metadata.
Definition: metadata.php:9
$query
buildSearchWhereQuery(QueryBuilder $qb, $alias, $fields, $query_parts, $partial_match=true)
Builds search clause.
normalizeSearchFields(array $options=[])
Normalizes an array of search fields.
$qb
Definition: queue.php:12
$attributes
Elgg AJAX loader.
Definition: ajax_loader.php:10
Builds clauses for filtering entities by properties in metadata table.