/[drupal]/contributions/modules/sphinxsearch/sphinxsearch.taxonomy.inc
ViewVC logotype

Contents of /contributions/modules/sphinxsearch/sphinxsearch.taxonomy.inc

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.3 - (show annotations) (download) (as text)
Fri Sep 12 02:44:22 2008 UTC (14 months, 2 weeks ago) by markuspetrux
Branch: MAIN
CVS Tags: HEAD
Branch point for: DRUPAL-6--1
Changes since 1.2: +117 -90 lines
File MIME type: text/x-php
- Ported module from D5 to D6.
- Bugfix: undefined class method in sphinxsearch_check_connection_page().
- Bugfix: added criterion class to taxonomy elements in advanced search form.
- Bugfix: added support for mysqli and pgsql to _sphinxsearch_db_reconnect().
1 <?php
2 // $Id: sphinxsearch.taxonomy.inc,v 1.1.2.4 2008/09/03 01:38:55 markuspetrux Exp $
3
4 /**
5 * @file
6 * Common taxonomy related functions for the Sphinx search module.
7 */
8
9 /**
10 * Obtain list of enabled vocabularies.
11 *
12 * Each vocabulary will be indexed on a separate multi-valued field.
13 *
14 * @return array
15 * list of enabled vocabularies.
16 */
17 function sphinxsearch_get_enabled_vocabularies() {
18 static $vocabularies;
19 if (!isset($vocabularies)) {
20 $vocabularies = array();
21 if (module_exists('taxonomy')) {
22 foreach(taxonomy_get_vocabularies() as $vocabulary) {
23 if (variable_get('sphinxsearch_include_vocabulary_'. $vocabulary->vid, 0)) {
24 $vocabularies[(int)$vocabulary->vid] = $vocabulary;
25 }
26 }
27 }
28 }
29 return $vocabularies;
30 }
31
32 /**
33 * Encode terms hash into a comma separated list.
34 *
35 * @param array $terms
36 * Hash of terms.
37 * @param string $separator
38 * Terms separator. Defaults to ', '.
39 * @return string
40 * Comma separated list of terms.
41 */
42 function sphinxsearch_taxonomy_encode_typed_terms($terms, $separator = ', ') {
43 $typed_terms = array();
44 foreach ($terms as $tid => $term) {
45 // Commas and quotes in terms are special cases, so encode 'em.
46 if (strpos($term->name, ',') !== FALSE || strpos($term->name, '"') !== FALSE) {
47 $term->name = '"'. str_replace('"', '""', $term->name) .'"';
48 }
49 $typed_terms[] = $term->name;
50 }
51 return implode($separator, $typed_terms);
52 }
53
54 /**
55 * Decode a comma separated list of strings into hash of terms.
56 * @see taxonomy_node_save()
57 *
58 * @param int $vid
59 * Vocabulary identifier related to terms.
60 * @param string $typed_terms
61 * Comma separated list of terms.
62 * @return array
63 * Hash of terms. Note that a string of not found typed terms is stored as tid -1.
64 */
65 function sphinxsearch_taxonomy_decode_typed_terms($vid, $typed_terms) {
66 $terms = array();
67 $regexp = '%(?:^|,\ *)("(?>[^"]*)(?>""[^"]* )*"|(?: [^",]*))%x';
68 preg_match_all($regexp, $typed_terms, $matches);
69 $typed_terms = array_unique($matches[1]);
70 foreach ($typed_terms as $typed_term) {
71 $typed_term = trim(str_replace('""', '"', preg_replace('#^"(.*)"$#', '\1', $typed_term)));
72 if (!empty($typed_term)) {
73 $possibilities = taxonomy_get_term_by_name($typed_term);
74 $term = NULL;
75 foreach ($possibilities as $possibility) {
76 if ($possibility->vid == $vid) {
77 $term = $possibility;
78 }
79 }
80 if ($term) {
81 $terms[$term->tid] = $term;
82 }
83 else {
84 // Store typed terms that we haven't found in a particular item with tid = -1,
85 // so caller has the chance to notify the user about it.
86 if (!isset($terms[-1])) {
87 $terms[-1] = array();
88 }
89 $terms[-1][] = $typed_term;
90 }
91 }
92 }
93 if (isset($terms[-1])) {
94 $terms[-1] = implode(', ', $terms[-1]);
95 }
96 return $terms;
97 }
98
99 /**
100 * Get term data for the given tids.
101 *
102 * @param int $vid
103 * Vocabulary identifier related to terms.
104 * @param array $tids
105 * List of tids.
106 * @return array
107 * Hash of terms. Note that a string of not found tids is stored as tid -1.
108 */
109 function sphinxsearch_taxonomy_get_terms($vid, $tids) {
110 $terms = array();
111 if (!empty($tids)) {
112 $query_args = array_merge(array($vid), $tids);
113 $query_sql = 'SELECT t.* FROM {term_data} t WHERE t.vid = %d AND t.tid ';
114 if (count($tids) == 1) {
115 $query_sql .= '= %d';
116 }
117 else {
118 $query_sql .= 'IN ('. db_placeholders($tids) .')';
119 }
120 $result = db_query(db_rewrite_sql($query_sql, 't', 'tid'), $query_args);
121 while ($term = db_fetch_object($result)) {
122 $terms[$term->tid] = $term;
123 }
124 $diff = array_diff($tids, array_keys($terms));
125 if (!empty($diff)) {
126 $terms[-1] = implode(', ', $diff);
127 }
128 }
129 return $terms;
130 }
131
132 /**
133 * Build tagadelic items for the given options.
134 * Tagadelic block (weighted tags cloud).
135 *
136 * @param array $build_options
137 * array(
138 * 'type' string Grouping type: 'author', 'taxonomy', 'types'.
139 * 'vid' int Vocabulary ID when grouping type is 'taxonomy'.
140 * 'count' int Number of tags to render.
141 * 'levels' int Amount of tag-sizes.
142 * 'sortmode' string Sort order options.
143 * 'request_options' array Requested search options (optional).
144 * '_faceted_data' boolean TRUE when used from sphinxsearch_faceted_build_data().
145 * )
146 *
147 * @return array
148 * Hash of tagadelic items.
149 */
150 function sphinxsearch_tagadelic_build_data($build_options) {
151 // Check options.
152 if (!isset($build_options['count']) || !is_numeric($build_options['count'])) {
153 $build_options['count'] = 20;
154 }
155 if (!isset($build_options['levels']) || !is_numeric($build_options['levels'])) {
156 $build_options['levels'] = 10;
157 }
158 if (!isset($build_options['sortmode'])) {
159 $build_options['sortmode'] = 'title,asc';
160 }
161 if (!isset($build_options['request_options']) || !is_array($build_options['request_options'])) {
162 $build_options['request_options'] = array();
163 }
164
165 // Build search options structure.
166 $search_options = sphinxsearch_parse_request($build_options['request_options']);
167 $search_filters = array();
168 if ($build_options['type'] == 'author') {
169 if (!empty($search_options['filters']['author'])) {
170 $author = new stdClass();
171 $author->uid = $search_options['filters']['author']['uid'];
172 $author->name = $search_options['filters']['author']['name'];
173 $search_filters[$author->uid] = $author;
174 }
175 $build_options['group_by_field'] = 'uid';
176 }
177 else if ($build_options['type'] == 'types') {
178 if (!isset($search_options['filters']['types'])) {
179 $search_options['filters']['types'] = array();
180 }
181 if (!empty($search_options['filters']['types'])) {
182 foreach ($search_options['filters']['types'] as $type) {
183 $node_type = new stdClass();
184 $node_type->type = $type;
185 $node_type->name = node_get_types('name', $type);
186 $search_filters[$node_type->type] = $node_type;
187 }
188 }
189 $build_options['group_by_field'] = 'nodetype';
190 }
191 else if ($build_options['type'] == 'taxonomy') {
192 $vid = (int)$build_options['vid'];
193 if (!isset($search_options['filters']['taxonomy'])) {
194 $search_options['filters']['taxonomy'] = array($vid => array());
195 }
196 else if (!isset($search_options['filters']['taxonomy'][$vid])) {
197 $search_options['filters']['taxonomy'][$vid] = array();
198 }
199 $search_filters = $search_options['filters']['taxonomy'][$vid];
200 $build_options['group_by_field'] = 'terms'. $vid;
201 }
202 else {
203 return FALSE;
204 }
205
206 // Results count should include number of items specified in filters.
207 $count = (int)$build_options['count'] + count($search_filters);
208
209 // Execute a GroupBy query based on current search options.
210 $search_results = sphinxsearch_execute_query(array_merge($search_options, array(
211 'results_per_page' => $count,
212 'group_by' => $build_options['group_by_field'],
213 )));
214 if ($search_results['total_available'] <= 0 && empty($build_options['_faceted_data'])) {
215 return FALSE;
216 }
217
218 // Remove filtered items from results.
219 $generic_groups = array();
220 foreach ($search_results['groups'] as $tid => $group_info) {
221 if (!isset($search_filters[$tid])) {
222 $generic_groups[$tid] = $group_info;
223 }
224 }
225 if (count($generic_groups) <= 1 && empty($build_options['_faceted_data'])) {
226 return FALSE;
227 }
228 unset($search_results);
229
230 // Compute filtered items list in user supplied order.
231 $filtered_items = array();
232 foreach ($search_filters as $tid => $item) {
233 if (!isset($generic_groups[$tid])) {
234 $filtered_items[$tid] = $item;
235 }
236 }
237
238 // Transform search results into tagadelic items.
239 $generic_terms = array();
240 if (!empty($generic_groups)) {
241 if ($build_options['type'] == 'author') {
242 $uids = array_keys($generic_groups);
243 $sql = 'SELECT uid, name FROM {users} WHERE uid ';
244 if (count($uids) == 1) {
245 $sql .= '= %d';
246 }
247 else {
248 $sql .= 'IN ('. db_placeholders($uids) .')';
249 }
250 $result = db_query($sql, $uids);
251 while ($row = db_fetch_object($result)) {
252 $generic_terms[$row->uid] = $row;
253 }
254 }
255 else if ($build_options['type'] == 'types') {
256 foreach (array_keys($generic_groups) as $type) {
257 $node_type = new stdClass();
258 $node_type->type = $type;
259 $node_type->name = node_get_types('name', $type);
260 $generic_terms[$node_type->type] = $node_type;
261 }
262 }
263 else if ($build_options['type'] == 'taxonomy') {
264 $generic_terms = sphinxsearch_taxonomy_get_terms($vid, array_keys($generic_groups));
265 }
266 }
267
268 // Find minimum and maximum log-count. Algorithm based on tagadelic.module.
269 $tags = array();
270 $min = 1e9;
271 $max = -1e9;
272 foreach ($generic_groups as $tid => $group_info) {
273 if (isset($generic_terms[$tid])) {
274 $tag = $generic_terms[$tid];
275 $tag->tagadelic_items = $group_info['count'];
276 $tag->tagadelic_count = log($group_info['count']);
277 $min = min($min, $tag->tagadelic_count);
278 $max = max($max, $tag->tagadelic_count);
279 $tags[$tid] = $tag;
280 }
281 }
282 if (empty($tags) && empty($build_options['_faceted_data'])) {
283 return FALSE;
284 }
285 unset($generic_groups, $generic_terms);
286
287 // Note: we need to ensure the range is slightly too large to make sure even
288 // the largest element is rounded down.
289 $range = max(.01, $max - $min) * 1.0001;
290 foreach ($tags as $tid => $tag) {
291 $tags[$tid]->tagadelic_weight = 1 + floor((int)$build_options['levels'] * ($tag->tagadelic_count - $min) / $range);
292
293 // Compute faceted search path/query string for each tag.
294 if (!empty($build_options['linkto']) && $build_options['linkto'] == 'search') {
295 $faceted_search_options = $search_options;
296 if ($build_options['type'] == 'author') {
297 $faceted_search_options['filters']['author'] = array('uid' => $tag->uid, 'name' => $tag->name);
298 }
299 else if ($build_options['type'] == 'types') {
300 $faceted_search_options['filters']['types'][] = $tag->type;
301 }
302 else if ($build_options['type'] == 'taxonomy') {
303 $vid = $build_options['vid'];
304 if (!isset($faceted_search_options['filters']['taxonomy'][$vid][$tid])) {
305 $faceted_search_options['filters']['taxonomy'][$vid][$tid] = $tag;
306 }
307 }
308 $tags[$tid]->faceted_path = sphinxsearch_get_search_path();
309 $tags[$tid]->faceted_query = sphinxsearch_get_query_string($faceted_search_options);
310 }
311 }
312
313 // Sort tags.
314 list($sort_by, $sort_order) = explode(',', $build_options['sortmode']);
315 switch ($sort_by) {
316 case 'title':
317 usort($tags, create_function('$a,$b', 'return strnatcasecmp($a->name, $b->name);'));
318 break;
319 case 'weight':
320 usort($tags, create_function('$a,$b', 'return $a->tagadelic_weight > $b->tagadelic_weight;'));
321 break;
322 case 'random':
323 shuffle($tags);
324 break;
325 }
326 if ($sort_order == 'desc') {
327 $tags = array_reverse($tags, TRUE);
328 }
329
330 // Additional faceted search processing (internal use only).
331 // @see sphinxsearch_faceted_block()
332 if (!empty($build_options['_faceted_data'])) {
333 // Return weighted tags, filtered items and search options.
334 return array($tags, $filtered_items, $search_options);
335 }
336
337 return $tags;
338 }
339
340 /**
341 * Menu callback; Tagadelic page (weighted tags cloud).
342 *
343 * @param string $vids
344 */
345 function sphinxsearch_tagadelic_page($vids = NULL) {
346 $vocabularies = sphinxsearch_get_enabled_vocabularies();
347 if (!isset($vids)) {
348 $vids = array_keys($vocabularies);
349 }
350 else {
351 $vids = array_filter(array_map('intval', array_map('trim', explode(',', $vids))));
352 $vids = array_intersect(array_keys($vocabularies), array_unique($vids));
353 }
354 $blocks = array();
355 foreach ($vids as $vid) {
356 $tags = sphinxsearch_tagadelic_build_data(array(
357 'type' => 'taxonomy', 'vid' => $vid,
358 'count' => (int)variable_get('sphinxsearch_page_tagadelic_tags', 100),
359 'levels' => (int)variable_get('sphinxsearch_page_tagadelic_levels', 10),
360 'sortmode' => variable_get('sphinxsearch_page_tagadelic_sortmode', 'title,asc'),
361 'linkto' => variable_get('sphinxsearch_page_tagadelic_linkto', 'taxonomy'),
362 ));
363 if (!empty($tags)) {
364 $blocks[$vid] = array(
365 'vocabulary' => $vocabularies[$vid],
366 'content' => theme('sphinxsearch_tagadelic_block', $tags),
367 );
368 }
369 }
370 if (empty($blocks)) {
371 drupal_not_found();
372 return;
373 }
374 return theme('sphinxsearch_tagadelic_page', $blocks);
375 }
376
377 /**
378 * Build faceted items for the given options.
379 *
380 * @param array $build_options
381 * array(
382 * 'count' int Number of tags to render per facet.
383 * 'levels' int Amount of tag-sizes.
384 * 'sortmode' string Sort order options.
385 * 'request_options' array Requested search options (optional).
386 * )
387 *
388 * @return array
389 * List of facets.
390 */
391 function sphinxsearch_faceted_build_data($build_options) {
392 // Check options.
393 if (!isset($build_options['count']) || !is_numeric($build_options['count'])) {
394 $build_options['count'] = 20;
395 }
396 if (!isset($build_options['levels']) || !is_numeric($build_options['levels'])) {
397 $build_options['levels'] = 10;
398 }
399 if (!isset($build_options['sortmode'])) {
400 $build_options['sortmode'] = 'title,asc';
401 }
402 if (!isset($build_options['request_options']) || !is_array($build_options['request_options'])) {
403 $build_options['request_options'] = $_GET;
404 }
405
406 // Append faceted search options.
407 $build_options['linkto'] = 'search';
408 $build_options['_faceted_data'] = TRUE;
409
410 $faceted_blocks = array();
411
412 // Build facets by taxonomy.
413 foreach (sphinxsearch_get_enabled_vocabularies() as $vid => $vocabulary) {
414 list($tags, $filtered_items, $search_options) = sphinxsearch_tagadelic_build_data(array_merge($build_options, array(
415 'type' => 'taxonomy',
416 'vid' => $vid,
417 )));
418 $faceted_blocks[] = array(
419 'type' => 'taxonomy', 'vid' => $vid,
420 'title' => $vocabulary->name,
421 'tags' => $tags,
422 'filtered_items' => $filtered_items,
423 'search_options' => $search_options,
424 );
425 }
426
427 // Build facets by node type.
428 list($tags, $filtered_items, $search_options) = sphinxsearch_tagadelic_build_data(array_merge($build_options, array(
429 'type' => 'types',
430 )));
431 $faceted_blocks[] = array(
432 'type' => 'types',
433 'title' => t('Content type'),
434 'tags' => $tags,
435 'filtered_items' => $filtered_items,
436 'search_options' => $search_options,
437 );
438
439 // Build facets by author.
440 list($tags, $filtered_items, $search_options) = sphinxsearch_tagadelic_build_data(array_merge($build_options, array(
441 'type' => 'author',
442 )));
443 $faceted_blocks[] = array(
444 'type' => 'author',
445 'title' => t('Author'),
446 'tags' => $tags,
447 'filtered_items' => $filtered_items,
448 'search_options' => $search_options,
449 );
450
451 $facets = array();
452 foreach ($faceted_blocks as $facet_data) {
453 if (empty($facet_data['tags']) && empty($facet_data['filtered_items'])) {
454 continue;
455 }
456
457 $facet = new stdClass();
458 $facet->type = $facet_data['type'];
459 $facet->title = $facet_data['title'];
460 $facet->tags = $facet_data['tags'];
461
462 // Compute search path/query string for facet filters.
463 if (!empty($facet_data['filtered_items'])) {
464 $reversed_terms = array_reverse($facet_data['filtered_items'], TRUE);
465 $remaining_terms = $facet_data['filtered_items'];
466 foreach ($reversed_terms as $tid => $term) {
467 $faceted_search_options = $facet_data['search_options'];
468 if ($facet_data['type'] == 'author') {
469 unset($faceted_search_options['filters']['author']);
470 }
471 else if ($facet_data['type'] == 'types') {
472 $faceted_search_options['filters']['types'] = array_keys($remaining_terms);
473 }
474 else if ($facet_data['type'] == 'taxonomy') {
475 $faceted_search_options['filters']['taxonomy'][$facet_data['vid']] = $remaining_terms;
476 }
477 $facet_data['filtered_items'][$tid]->faceted_path = sphinxsearch_get_search_path();
478 $facet_data['filtered_items'][$tid]->faceted_query = sphinxsearch_get_query_string($faceted_search_options);
479 unset($remaining_terms[$tid]);
480 }
481
482 $faceted_search_options = $facet_data['search_options'];
483 if ($facet_data['type'] == 'author') {
484 unset($faceted_search_options['filters']['author']);
485 }
486 else if ($facet_data['type'] == 'types') {
487 unset($faceted_search_options['filters']['types']);
488 }
489 else if ($facet_data['type'] == 'taxonomy') {
490 unset($faceted_search_options['filters']['taxonomy'][$facet_data['vid']]);
491 }
492 $facet->path = sphinxsearch_get_search_path();
493 $facet->query = sphinxsearch_get_query_string($faceted_search_options);
494 $facet->terms = $facet_data['filtered_items'];
495 }
496
497 $facets[] = $facet;
498 }
499 return $facets;
500 }
501
502 /**
503 * Render faceted search block for the given facets.
504 *
505 * @param array $facets
506 *
507 * @ingroup themeable
508 */
509 function theme_sphinxsearch_faceted_block($facets) {
510 $output = '';
511
512 // Build superset section.
513 $superset = array();
514 foreach ($facets as $facet) {
515 if (!empty($facet->terms)) {
516 $links = array();
517 $last_term = array_pop($facet->terms);
518 foreach ($facet->terms as $tid => $term) {
519 $url = url($term->faceted_path, array('query' => $term->faceted_query));
520 $link = '<span><a href="'. $url .'">'. check_plain($term->name) .'</a></span>';
521 $links[] = $link;
522 }
523 $links[] = '<span class="faceted-superset-youarehere">'. check_plain($last_term->name) .'</span>';
524 if (!empty($links)) {
525 $url = url($facet->path, array('query' => $facet->query));
526 $link = '<span><a href="'. $url .'">'. t('All') .'</a></span>';
527 $superset[] = '<div><strong>'. check_plain($facet->title) .':</strong> '. implode(' › ', array_merge(array($link), $links)) .'</div>';
528 }
529 }
530 }
531 if (!empty($superset)) {
532 $output .= '<div class="faceted-superset"><h2>'. t('You are here') .':</h2><div class="faceted-superset-lists">'. theme('item_list', $superset) .'</div></div>';
533 }
534
535 // Build refine search by terms section.
536 foreach ($facets as $facet) {
537 if (!empty($facet->tags)) {
538 $facet_tags = theme('sphinxsearch_tagadelic_block', $facet->tags);
539 if (!empty($facet_tags)) {
540 $output .= '<div class="faceted-vocabulary"><h2>'. t('Refine search by: @vocabulary', array('@vocabulary' => $facet->title)) .'</h2>'. $facet_tags .'</div>' ."\n";
541 }
542 }
543 }
544
545 if (empty($output)) {
546 return '';
547 }
548 return '<div class="faceted-wrapper">'. $output .'</div>';
549 }
550
551 /**
552 * Render tagadelic block for the given tags.
553 *
554 * @param array $tags
555 *
556 * @ingroup themeable
557 */
558 function theme_sphinxsearch_tagadelic_block($tags) {
559 $output = '';
560
561 foreach ($tags as $tag) {
562 // Note: we don't use l() because it may append class="active".
563 $attributes = array('class' => 'tagadelic-level'. $tag->tagadelic_weight, 'rel' => (!empty($tag->faceted_path) ? 'nofollow' : 'tag'));
564 if (!empty($tag->tagadelic_items)) {
565 $attributes['title'] = $tag->name .' ('. $tag->tagadelic_items .')';
566 }
567 $url = (!empty($tag->faceted_path) ? url($tag->faceted_path, array('query' => $tag->faceted_query)) : url(taxonomy_term_path($tag)));
568 $output .= '<a href="'. $url .'"'. drupal_attributes($attributes) .'>'. check_plain($tag->name) .'</a>' ."\n";
569 }
570
571 if (empty($output)) {
572 return '';
573 }
574 return '<div class="tagadelic-wrapper">'. $output .'</div>';
575 }
576
577 /**
578 * Render more link for tagadelic block.
579 *
580 * @param int $vid
581 *
582 * @ingroup themeable
583 */
584 function theme_sphinxsearch_tagadelic_more($vid) {
585 return '<div class="more-link">'. l(t('more'), 'tagadelic/'. $vid) .'</div>';
586 }
587
588 /**
589 * Render tagadelic page for the given blocks.
590 *
591 * @param array $blocks
592 * Hash of blocks per vocabulary. Elements:
593 * - vocabulary: object
594 * - content: Tagadelic block content in HTML format.
595 *
596 * @ingroup themeable
597 */
598 function theme_sphinxsearch_tagadelic_page($blocks) {
599 $output = '';
600 $blocks_count = count($blocks);
601 foreach ($blocks as $vid => $block) {
602 $output .= '<div class="tagadelic-page-block">';
603 if ($blocks_count == 1) {
604 drupal_set_title(t('Tags in @vocabulary', array('@vocabulary' => $block['vocabulary']->name)));
605 }
606 else {
607 $output .= '<h2>'. check_plain($block['vocabulary']->name) .'</h2>';
608 }
609 $output .= $block['content'] .'</div>';
610 }
611 return $output;
612 }

  ViewVC Help
Powered by ViewVC 1.1.2