CWIS Developer Documentation
SearchFacetUI.php
Go to the documentation of this file.
1 <?PHP
2 #
3 # FILE: SearchFacetUI.php
4 #
5 # Part of the Collection Workflow Integration System (CWIS)
6 # Copyright 2015 Edward Almasy and Internet Scout Research Group
7 # http://scout.wisc.edu/cwis/
8 #
9 
16 {
17 
18  # ---- PUBLIC INTERFACE --------------------------------------------------
19 
32  public function __construct($BaseLink, $SearchParams, $SearchResults,
33  $User, $MaxFacetsPerField, $SchemaId)
34  {
35  # save base URL, search parameters, and max facets for later use
36  $this->BaseLink = $BaseLink;
37  $this->SearchParams = $SearchParams;
38  $this->MaxFacetsPerField = $MaxFacetsPerField;
39 
40  # facets which should be displayed because they were explicitly
41  # included in the search terms
42  $this->FieldsOpenByDefault = array();
43 
44  # suggestions for Option and ControlledName fields
45  # array( FieldName => array(ModifiedSearchUrls)
46  $this->SuggestionsByFieldName = array();
47 
48  # suggestions for Tree fields
49  # when nothing is selected,
50  # array( FieldName => array(ModifiedSearchUrls)) {exactly as above}
51  # when something is selected and we should display its children
52  # array( FieldName => array( Selection => array(ModifiedSearchURLs)))
53  # where the last Modified URL is always a link to remove the
54  # current selection
55  $this->TreeSuggestionsByFieldName = array();
56 
57  # iterrate over the suggested result facets, bulding up a list of
58  # those we care to display. this is necessary because GetResultFacets
59  # just gives back a list of all Tree, Option, and ControlledNames that
60  # occur in the result set. For Trees in particular, some processing
61  # is necessary as we don't want to just display a value six layers deep
62  # when our current search has selected something at the second layer of
63  # the tree.
64  $Schema = new MetadataSchema($SchemaId);
65  $ResultFacets = SPTSearchEngine::GetResultFacets($SearchResults, $User);
66  foreach ($ResultFacets as $FieldName => $Suggestions)
67  {
68  if (!$Schema->FieldExists($FieldName))
69  {
70  continue;
71  }
72 
73  $Field = $Schema->GetField($FieldName);
74  $FieldId = $Field->Id();
75 
76  # foreach field, generate a list of suggested facets
77  # and determine if the field should be open by default
78  if ($Field->Type() == MetadataSchema::MDFTYPE_TREE)
79  {
80  $Suggestions = $this->GenerateFacetsForTree($Field, $Suggestions);
81  }
82  else
83  {
84  $this->GenerateFacetsForName($Field, $Suggestions);
85  }
86  }
87 
88  # Make sure that we've got a facet displayed for each selected search option
89  # (including those that were not suggested as facets)
90 
91  $TreeFieldsToCheck = array();
92  # for each field in search parameters
93  foreach ($this->SearchParams->GetSearchStrings(TRUE)
94  as $FieldId => $Values)
95  {
96  # if the field does not exist, move to the next one
97  if (!$Schema->FieldExists($FieldId))
98  {
99  continue;
100  }
101 
102  # if field is valid and viewable
103  $Field = $Schema->GetField($FieldId);
104  if (is_object($Field)
105  && ($Field->Status() === MetadataSchema::MDFSTAT_OK)
106  && $Field->UserCanView($User))
107  {
108  # for each value for field
109  $FieldName = $Field->Name();
110  foreach ($Values as $Value)
111  {
112  # for Tree fields
113  if ($Field->Type() == MetadataSchema::MDFTYPE_TREE)
114  {
115  # if value is not already in list
116  if (!isset($this->DisplayedValues[$Field->Name()][$Value]))
117  {
118  $TreeFieldsToCheck[$Field->Name()][]= $Value;
119  # allow removing the current search terms
120  $this->FieldsOpenByDefault[$FieldName] = TRUE;
121  $RemovalLink = $this->RemoveTermFromSearchURL(
122  $Field, $Value);
123  $this->TreeSuggestionsByFieldName[$FieldName][$Value][]
124  = array(
125  "Name" => $Value,
126  "RemoveLink" => $RemovalLink);
127  }
128  }
129  # for Option and Controlled Name fields
130  elseif ($Field->Type() == MetadataSchema::MDFTYPE_OPTION
131  || $Field->Type() == MetadataSchema::MDFTYPE_CONTROLLEDNAME)
132  {
133  # if this is not a "contains" search parameter
134  if ($Value[0] == "=")
135  {
136  # if value is not already in list
137  if (!isset($this->DisplayedValues[$Field->Name(
138  )][substr($Value, 1)]))
139  {
140  # note that this field should be open
141  $this->FieldsOpenByDefault[$FieldName] = TRUE;
142 
143  # mark as a facet that can be removed
144  $RemovalLink = $this->RemoveTermFromSearchURL(
145  $Field, $Value);
146  $this->SuggestionsByFieldName[$Field->Name()][] = array(
147  "Name" => substr($Value, 1),
148  "RemoveLink" => $RemovalLink);
149  }
150  }
151  }
152  }
153  }
154  }
155 
156  # next, iterate over tree fields and check to see if we need
157  # to clean up the link suggestions
158  foreach ($TreeFieldsToCheck as $FieldName => $Values)
159  {
160  # if we have two conditions for this field, the first one
161  # is an exact match, the second one is a prefix match, and
162  # they refer to the same string then we need to clean this
163  # one up
164  if (count($Values) == 2 &&
165  preg_match('/^=(.*)$/', $Values[0], $FirstMatch) &&
166  preg_match('/^\\^(.*) -- $/', $Values[1], $SecondMatch) &&
167  $FirstMatch[1] == $SecondMatch[1] )
168  {
169  # erase the two separate remove links
170  unset ($this->TreeSuggestionsByFieldName[$FieldName][$Values[0]]);
171  unset ($this->TreeSuggestionsByFieldName[$FieldName][$Values[1]]);
172 
173  # pull out our field
174  $Field = $Schema->GetField($FieldName);
175 
176  # generate a remove link that covers both conditions
177  $RemovalLink = $this->RemoveTermFromSearchUrl(
178  $Field, array($Values[0], $Values[1]) );
179 
180  # add that link in to our suggestions
181  $this->TreeSuggestionsByFieldName[$FieldName][$FirstMatch[1]][]=
182  array("Name" => $FirstMatch[1],
183  "RemoveLink" => $RemovalLink);
184  }
185  }
186 
187  # within each field, sort the suggestions alphabetically
188  foreach ($this->TreeSuggestionsByFieldName as &$Suggestion)
189  {
190  uasort($Suggestion, array($this, "FacetSuggestionSortCallback"));
191  }
192  foreach ($this->SuggestionsByFieldName as &$Suggestion)
193  {
194  uasort($Suggestion, array($this, "FacetSuggestionSortCallback"));
195  }
196  }
197 
202  public function GetSuggestionsByFieldName()
203  {
204  return $this->SuggestionsByFieldName;
205  }
206 
212  {
213  return $this->TreeSuggestionsByFieldName;
214  }
215 
220  public function GetFieldsOpenByDefault()
221  {
222  return $this->FieldsOpenByDefault;
223  }
224 
225 
226  # ---- PRIVATE INTERFACE -------------------------------------------------
227 
228  private $BaseLink;
229  private $DisplayedValues;
230  private $FieldsOpenByDefault;
231  private $MaxFacetsPerField;
232  private $SearchParams;
233  private $SuggestionsByFieldName;
234  private $TreeSuggestionsByFieldName;
235 
241  private function GenerateFacetsForTree($Field, $Suggestions)
242  {
243  # if we already have selections for this field, dig out the children of that
244  # selection and present those as options:
245  $CurrentValues = $this->SearchParams->GetSearchStringsForField($Field);
246  if (count($CurrentValues))
247  {
248  $this->FieldsOpenByDefault[$Field->Name()] = TRUE;
249  $ParentValue = $CurrentValues[0];
250 
251  # if this was an exact match, rather than a 'contains'
252  if (strpos($ParentValue, "=") === 0)
253  {
254  # cut off the leading '='
255  $ParentValue = substr($ParentValue, 1);
256  }
257 
258  $FacetsForThisTree = $this->GenerateFacetsForSelectedTree(
259  $Field, $Suggestions, $ParentValue);
260 
261  # if we have facets to display, but not too many of them
262  if ((count($FacetsForThisTree) > 0)
263  && (count($FacetsForThisTree) <= $this->MaxFacetsPerField))
264  {
265  # and append the required 'remove current' link
266  $RemovalLink = $this->RemoveTermFromSearchURL($Field, $CurrentValues);
267  $FacetsForThisTree[] = array(
268  "Name" => $ParentValue,
269  "RemoveLink" => $RemovalLink);
270 
271  # mark each value for this field displayed
272  foreach ($CurrentValues as $Val)
273  {
274  $this->DisplayedValues[$Field->Name()][$Val] = TRUE;
275  }
276 
277  $this->TreeSuggestionsByFieldName[$Field->Name()] =
278  array($FacetsForThisTree);
279  }
280  }
281  else
282  {
283  # otherwise, list the toplevel options for this field
284  $FacetsForThisTree = $this->GenerateFacetsForUnselectedTree(
285  $Field, $Suggestions);
286 
287  # if we have facets to display, but not too many add the toplevel options
288  # to the suggestions for this field
289  if ((count($FacetsForThisTree) > 0)
290  && (count($FacetsForThisTree) <= $this->MaxFacetsPerField))
291  {
292  $this->TreeSuggestionsByFieldName[$Field->Name()] =
293  $FacetsForThisTree;
294  }
295  }
296  }
297 
306  private function GenerateFacetsForSelectedTree($Field, $Suggestions, $ParentValue)
307  {
308  $ParentSegments = explode(" -- ", $ParentValue);
309 
310  # keep track of what has been shown
311  $DisplayedSegments = array();
312 
313  $FacetsForThisTree = array();
314  # pull out those classifications which were explicitly assigned to something.
315  # those are the ones we can get counts for:
316  $FieldName = $Field->Name();
317  foreach ($Suggestions as $ValueId => $ValueData)
318  {
319  $ValueName = $ValueData["Name"];
320  $ValueCount = $ValueData["Count"];
321  $FieldSegments = explode(" -- ", $ValueName);
322 
323  # print any children of the current field value
324  # (determined based on a prefix match
325  if ( preg_match("/^".preg_quote($ParentValue." -- ", "/")."/", $ValueName)
326  && (count($FieldSegments) == (count($ParentSegments) + 1)))
327  {
328  # note that we've already displayed this segment because it was selected
329  # allowing us to avoid displaying it twice
330  $DisplayedSegments[$ValueName]=TRUE;
331  $LeafValue = array_pop($FieldSegments);
332 
333  # add the modified search URL to our list of facets for this field
334  $AdditionLink = $this->AddTermToSearchURL($Field, $ValueName);
335 
336  $FacetsForThisTree[] = array(
337  "Name" => $LeafValue,
338  "AddLink" => $AdditionLink,
339  "Count" => $ValueCount);
340 
341  # record value as having been displayed
342  $this->DisplayedValues[$FieldName][$LeafValue] = TRUE;
343  }
344  }
345 
346  # now, we'll need to iterate over the suggestions again and make sure that we've
347  # displayed proper suggestions even for fields where only the lowest level
348  # elements are displayed
349  foreach ($Suggestions as $ValueId => $ValueData)
350  {
351  $ValueName = $ValueData["Name"];
352  $ValueCount = $ValueData["Count"];
353 
354  # get an array of the segments for this suggestion
355  $FieldSegments = explode(" -- ", $ValueName);
356 
357  # skip segments that are along some other branch of this tree
358  if (!preg_match("/^".preg_quote($ParentValue." -- ", "/")
359  ."/", $ValueName))
360  {
361  continue;
362  }
363 
364  # skip segments that aren't (grand)* children of the current parent.
365  if (count($FieldSegments) < count($ParentSegments))
366  {
367  continue;
368  }
369 
370  # truncate our array of segments to include only the
371  # level below our currently selected level
372  $TargetSegments = array_slice($FieldSegments, 0,
373  (count($ParentSegments) + 1));
374 
375  # reassemble this into a Classification string
376  $TargetName = implode(" -- ", $TargetSegments);
377 
378  # if we haven't already displayed this string, either
379  # because it was in an earlier suggestion or was
380  # actually included in the search, add it to our list
381  # now
382  if (!isset($DisplayedSegments[$TargetName]))
383  {
384  # as above, note that we've displayed this segment
385  # and add a modified search URL to our list of
386  # facets to display
387  $DisplayedSegments[$TargetName] = TRUE;
388  $LeafValue = array_pop($TargetSegments);
389  $AdditionLink = $this->AddTermToSearchURL($Field, $TargetName);
390  $FacetsForThisTree[] = array(
391  "Name" => $LeafValue,
392  "AddLink" => $AdditionLink);
393 
394  # record value as having been displayed
395  $this->DisplayedValues[$FieldName][$LeafValue] = TRUE;
396  }
397  }
398 
399  return $FacetsForThisTree;
400  }
401 
409  private function GenerateFacetsForUnselectedTree($Field, $Suggestions)
410  {
411  $FacetsForThisTree = array();
412 
413  # keep track of what has been already shown
414  $DisplayedSegments = array();
415 
416  # first, pull out those which are assigned to some reasources and for which
417  # we can compute counts (those with the top-level directly assigned)
418  $FieldName = $Field->Name();
419  foreach ($Suggestions as $ValueId => $ValueData )
420  {
421  $ValueName = $ValueData["Name"];
422  $ValueCount = $ValueData["Count"];
423  $FieldSegments = explode(" -- ", $ValueName);
424 
425  # if this is a top level field
426  if (count($FieldSegments) == 1)
427  {
428  # add it to our list of facets for this tree
429  $DisplayedSegments[$ValueName] = TRUE;
430  $AdditionLink = $this->AddTermToSearchURL($Field, $ValueName);
431  $FacetsForThisTree[] = array(
432  "Name" => $ValueName,
433  "AddLink" => $AdditionLink,
434  "Count" => $ValueCount);
435 
436  # record value as having been displayed
437  $this->DisplayedValues[$FieldName][$ValueName] = TRUE;
438  }
439  }
440 
441  # scan through the suggestions again, and verify that
442  # we've at least offered the top-level
443  # option for each entry (necessary for cases where no
444  # resources in the results set have the top-level value,
445  # e.g. they all have Science -- Technology but none have
446  # Science. in this case we can't display counts, but
447  # should at least suggest Science as an option)
448  foreach ($Suggestions as $ValueId => $ValueData)
449  {
450  $ValueName = $ValueData["Name"];
451  $FieldSegments = explode(" -- ", $ValueName);
452 
453  # if none of our previous efforts have displayed this segment
454  if (!isset($DisplayedSegments[$FieldSegments[0]]))
455  {
456  # add it to our list
457  $DisplayedSegments[$FieldSegments[0]] = TRUE;
458  $AdditionLink = $this->AddTermToSearchURL(
459  $Field, $FieldSegments[0]);
460  $FacetsForThisTree[] = array(
461  "Name" => $FieldSegments[0],
462  "AddLink" => $AdditionLink,
463  "Count" => $ValueData["Count"]);
464 
465  # record value as having been displayed
466  $this->DisplayedValues[$FieldName][$FieldSegments[0]] = TRUE;
467  }
468  }
469 
470  return $FacetsForThisTree;
471  }
472 
479  private function GenerateFacetsForName($Field, $Suggestions)
480  {
481  # for option fields, bail when there are too many suggestions for this option
482  if (count($Suggestions) > $this->MaxFacetsPerField)
483  {
484  return;
485  }
486 
487  # retrieve current search parameter values for field
488  $CurrentValues = $this->SearchParams->GetSearchStringsForField($Field);
489 
490  # if a field is required, and we have only one suggestion and
491  # there is no current value, then we don't want to display this
492  # facet because the search results will be identical
493  if (($Field->Optional() == FALSE)
494  && (count($Suggestions) == 1)
495  && !count($CurrentValues))
496  {
497  return;
498  }
499 
500  # for each suggested value
501  $FieldName = $Field->Name();
502  foreach ($Suggestions as $ValueId => $ValueData)
503  {
504  $ValueName = $ValueData["Name"];
505 
506  # if we have a current value that is selected for this search
507  if (in_array("=".$ValueName, $CurrentValues))
508  {
509  # note that this facet should be displayed and add a
510  # remove link for it
511  $this->FieldsOpenByDefault[$FieldName] = TRUE;
512  $RemovalLink = $this->RemoveTermFromSearchURL(
513  $Field, "=".$ValueName);
514  $this->SuggestionsByFieldName[$FieldName][] = array(
515  "Name" => $ValueName,
516  "RemoveLink" => $RemovalLink,
517  "Count" => $ValueData["Count"]);
518  }
519  else
520  {
521  # otherwise, put an 'add' link in our suggestions,
522  # displaying counts if we can
523  $AdditionLink = $this->AddTermToSearchURL(
524  $Field, "=".$ValueName);
525  $this->SuggestionsByFieldName[$FieldName][] = array(
526  "Name" => $ValueName,
527  "AddLink" => $AdditionLink,
528  "Count" => $ValueData["Count"]);
529  }
530 
531  # record value as having been displayed
532  $this->DisplayedValues[$FieldName][$ValueName] = TRUE;
533  }
534  }
535 
543  private function AddTermToSearchURL($Field, $Term)
544  {
545  # create our own copy of search parameters
546  $OurSearchParams = clone $this->SearchParams;
547 
548  # if this is not a tree field type
549  if ($Field->Type() != MetadataSchema::MDFTYPE_TREE)
550  {
551  # retrieve subgroups from search parameters
552  $Subgroups = $OurSearchParams->GetSubgroups();
553 
554  # find subgroup for this field
555  foreach ($Subgroups as $Group)
556  {
557  if (in_array($Field->Id(), $Group->GetFields()))
558  {
559  $Subgroup = $Group;
560  break;
561  }
562  }
563 
564  # if subgroup found
565  if (isset($Subgroup))
566  {
567  # add term to subgroup
568  $Subgroup->AddParameter($Term, $Field);
569  }
570  else
571  {
572  # create new subgroup
573  $Subgroup = new SearchParameterSet();
574 
575  # set logic for new subgroup
576  $Subgroup->Logic($Field->SearchGroupLogic());
577 
578  # add term to subgroup
579  $Subgroup->AddParameter($Term, $Field);
580 
581  # add subgroup to search parameters
582  $OurSearchParams->AddSet($Subgroup);
583  }
584  }
585  else
586  {
587  # add specified term to search parameters
588  $OurSearchParams->RemoveParameter(NULL, $Field);
589  $Subgroup = new SearchParameterSet();
590  $Subgroup->Logic("OR");
591  $Subgroup->AddParameter(array(
592  "=".$Term, "^".$Term." -- "), $Field);
593  $OurSearchParams->AddSet($Subgroup);
594  }
595 
596  # build new URL with revised search parameters
597  $Url = implode("&amp;", array_filter(array(
598  $this->BaseLink, $OurSearchParams->UrlParameterString())));
599 
600  # return new URL to caller
601  return $Url;
602  }
603 
611  private function RemoveTermFromSearchURL($Field, $Terms)
612  {
613  # create our own copy of search parameters with specified parameter removed
614  $OurSearchParams = clone $this->SearchParams;
615 
616  if ($Field->Type() == MetadataSchema::MDFTYPE_TREE)
617  {
618  # pull out the old setting
619  $OldSetting = $OurSearchParams->GetSearchStringsForField($Field);
620  $OurSearchParams->RemoveParameter($Terms, $Field);
621 
622  # if we had an exact + prefix match for a specified level of the tree,
623  # move up one level
624  if (count($OldSetting) == 2 &&
625  preg_match('/^=(.*)$/', $OldSetting[0], $FirstMatch) &&
626  preg_match('/^\\^(.*) -- $/', $OldSetting[1], $SecondMatch) &&
627  $FirstMatch[1] == $SecondMatch[1] )
628  {
629  # explode out the segments
630  $FieldSegments = explode(" -- ", $FirstMatch[1]);
631 
632  # remove the last one
633  array_pop($FieldSegments);
634 
635  # if we have any segments left
636  if (count($FieldSegments))
637  {
638  # construct revised setting
639  $NewSetting = implode(" -- ", $FieldSegments);
640  $Subset = new SearchParameterSet();
641  $Subset->Logic("OR");
642  $Subset->AddParameter(
643  array("=".$NewSetting, "^".$NewSetting." -- "),
644  $Field);
645  $OurSearchParams->AddSet($Subset);
646  }
647  }
648  }
649  else
650  {
651  $OurSearchParams->RemoveParameter($Terms, $Field);
652  }
653 
654  # build new URL with revised search parameters
655  $Url = implode("&amp;", array_filter(array(
656  $this->BaseLink, $OurSearchParams->UrlParameterString())));
657 
658  # return new URL to caller
659  return $Url;
660  }
661 
668  private function FacetSuggestionSortCallback($A, $B)
669  {
670  if (isset($A["Name"]))
671  {
672  return strcmp($A["Name"], $B["Name"]);
673  }
674  else
675  {
676  $CountA = count($A);
677  $CountB = count($B);
678  return ($CountA == $CountB) ? 0
679  : (($CountA < $CountB) ? -1 : 1);
680  }
681  }
682 }
GetFieldsOpenByDefault()
Retrieve which fields should be initially open in facet UI.
Metadata schema (in effect a Factory class for MetadataField).
Set of parameters used to perform a search.
GetSuggestionsByFieldName()
Retrieve facet UI data for non-tree metadata fields.
const MDFTYPE_CONTROLLEDNAME
__construct($BaseLink, $SearchParams, $SearchResults, $User, $MaxFacetsPerField, $SchemaId)
Constructor, that accepts the search parameters and results and prepares the facet UI data to be retr...
GetTreeSuggestionsByFieldName()
Retrieve facet UI data for tree metadata fields.
static GetResultFacets($SearchResults, $User)
Generate a list of suggested additional search terms that can be used for faceted searching...
SearchFacetUI supports the generation of a user interface for faceted search, by taking the search pa...