CWIS Developer Documentation
SPTSearchEngine.php
Go to the documentation of this file.
1 <?PHP
2 #
3 # FILE: SPTSearchEngine.php
4 #
5 # Part of the Collection Workflow Integration System (CWIS)
6 # Copyright 2011-2013 Edward Almasy and Internet Scout Research Group
7 # http://scout.wisc.edu/cwis/
8 #
9 
11 {
15  public function __construct()
16  {
17  # pass database handle and config values to real search engine object
18  parent::__construct("Resources", "ResourceId", "SchemaId");
19 
20  # for each schema
21  $Schemas = MetadataSchema::GetAllSchemas();
22  foreach ($Schemas as $SchemaId => $Schema)
23  {
24  # for each field defined in schema
25  $this->Schemas[$SchemaId] = new MetadataSchema($SchemaId);
26  $Fields = $this->Schemas[$SchemaId]->GetFields();
27  foreach ($Fields as $FieldId => $Field)
28  {
29  # save metadata field type
30  $this->FieldTypes[$FieldId] = $Field->Type();
31 
32  # determine field type for searching
33  switch ($Field->Type())
34  {
45  $FieldType = self::FIELDTYPE_TEXT;
46  break;
47 
50  $FieldType = self::FIELDTYPE_NUMERIC;
51  break;
52 
54  $FieldType = self::FIELDTYPE_DATERANGE;
55  break;
56 
58  $FieldType = self::FIELDTYPE_DATE;
59  break;
60 
62  $FieldType = NULL;
63  break;
64 
65  default:
66  throw Exception("ERROR: unknown field type "
67  .$Field->Type());
68  break;
69  }
70 
71  if ($FieldType !== NULL)
72  {
73  # add field to search engine
74  $this->AddField($FieldId, $FieldType, $Field->SchemaId(),
75  $Field->SearchWeight(),
76  $Field->IncludeInKeywordSearch());
77  }
78  }
79  }
80  }
81 
89  public function GetFieldContent($ItemId, $FieldId)
90  {
91  # get resource object
92  $Resource = new Resource($ItemId);
93 
94  # if this is a reference field
95  if ($this->FieldTypes[$FieldId] == MetadataSchema::MDFTYPE_REFERENCE)
96  {
97  # retrieve IDs of referenced items
98  $ReferredItemIds = $Resource->Get($FieldId);
99 
100  # for each referred item
101  $ReturnValue = array();
102  foreach ($ReferredItemIds as $RefId)
103  {
104  # retrieve title value for item and add to returned values
105  $RefResource = new Resource($RefId);
106  $ReturnValue[] = $RefResource->GetMapped("Title");
107  }
108 
109  # return referred item titles to caller
110  return $ReturnValue;
111  }
112  else
113  {
114  # retrieve text (including variants) from resource object and return to caller
115  return $Resource->Get($FieldId, FALSE, TRUE);
116  }
117  }
118 
125  public function SearchFieldForPhrases($FieldId, $Phrase)
126  {
127  # normalize and escape search phrase for use in SQL query
128  $SearchPhrase = strtolower(addslashes($Phrase));
129 
130  # query DB for matching list based on field type
131  $Field = new MetadataField($FieldId);
132  switch ($Field->Type())
133  {
138  $QueryString = "SELECT DISTINCT ResourceId FROM Resources "
139  ."WHERE POSITION('".$SearchPhrase."'"
140  ." IN LOWER(`".$Field->DBFieldName()."`)) ";
141  break;
142 
144  $QueryString = "SELECT DISTINCT ResourceId FROM Resources "
145  ."WHERE POSITION('".$SearchPhrase."'"
146  ." IN LOWER(`".$Field->DBFieldName()."AltText`)) ";
147  break;
148 
150  $NameTableSize = $this->DB->Query("SELECT COUNT(*) AS NameCount"
151  ." FROM ControlledNames", "NameCount");
152  $QueryString = "SELECT DISTINCT ResourceNameInts.ResourceId "
153  ."FROM ResourceNameInts, ControlledNames "
154  ."WHERE POSITION('".$SearchPhrase."' IN LOWER(ControlledName)) "
155  ."AND ControlledNames.ControlledNameId"
156  ." = ResourceNameInts.ControlledNameId "
157  ."AND ControlledNames.FieldId = ".intval($FieldId);
158  $SecondQueryString = "SELECT DISTINCT ResourceNameInts.ResourceId "
159  ."FROM ResourceNameInts, ControlledNames, VariantNames "
160  ."WHERE POSITION('".$SearchPhrase."' IN LOWER(VariantName)) "
161  ."AND VariantNames.ControlledNameId"
162  ." = ResourceNameInts.ControlledNameId "
163  ."AND ControlledNames.ControlledNameId"
164  ." = ResourceNameInts.ControlledNameId "
165  ."AND ControlledNames.FieldId = ".intval($FieldId);
166  break;
167 
169  $QueryString = "SELECT DISTINCT ResourceNameInts.ResourceId "
170  ."FROM ResourceNameInts, ControlledNames "
171  ."WHERE POSITION('".$SearchPhrase."' IN LOWER(ControlledName)) "
172  ."AND ControlledNames.ControlledNameId"
173  ." = ResourceNameInts.ControlledNameId "
174  ."AND ControlledNames.FieldId = ".intval($FieldId);
175  break;
176 
178  $QueryString = "SELECT DISTINCT ResourceClassInts.ResourceId "
179  ."FROM ResourceClassInts, Classifications "
180  ."WHERE POSITION('".$SearchPhrase
181  ."' IN LOWER(ClassificationName)) "
182  ."AND Classifications.ClassificationId"
183  ." = ResourceClassInts.ClassificationId "
184  ."AND Classifications.FieldId = ".intval($FieldId);
185  break;
186 
188  $UserId = $this->DB->Query("SELECT UserId FROM APUsers "
189  ."WHERE POSITION('".$SearchPhrase
190  ."' IN LOWER(UserName)) "
191  ."OR POSITION('".$SearchPhrase
192  ."' IN LOWER(RealName))", "UserId");
193  if ($UserId != NULL)
194  {
195  $QueryString = "SELECT DISTINCT ResourceId FROM ResourceUserInts "
196  ."WHERE UserId = ".$UserId
197  ." AND FieldId = ".intval($FieldId);
198  }
199  break;
200 
202  if ($SearchPhrase > 0)
203  {
204  $QueryString = "SELECT DISTINCT ResourceId FROM Resources "
205  ."WHERE `".$Field->DBFieldName()
206  ."` = ".(int)$SearchPhrase;
207  }
208  break;
209 
214  # (these types not yet handled by search engine for phrases)
215  break;
216  }
217 
218  # build match list based on results returned from DB
219  if (isset($QueryString))
220  {
221  $this->DMsg(7, "Performing phrase search query (<i>".$QueryString."</i>)");
222  if ($this->DebugLevel > 9) { $StartTime = microtime(TRUE); }
223  $this->DB->Query($QueryString);
224  if ($this->DebugLevel > 9)
225  {
226  $EndTime = microtime(TRUE);
227  if (($StartTime - $EndTime) > 0.1)
228  {
229  printf("SE: Query took %.2f seconds<br>\n",
230  ($EndTime - $StartTime));
231  }
232  }
233  $MatchList = $this->DB->FetchColumn("ResourceId");
234  if (isset($SecondQueryString))
235  {
236  $this->DMsg(7, "Performing second phrase search query"
237  ." (<i>".$SecondQueryString."</i>)");
238  if ($this->DebugLevel > 9) { $StartTime = microtime(TRUE); }
239  $this->DB->Query($SecondQueryString);
240  if ($this->DebugLevel > 9)
241  {
242  $EndTime = microtime(TRUE);
243  if (($StartTime - $EndTime) > 0.1)
244  {
245  printf("SE: query took %.2f seconds<br>\n",
246  ($EndTime - $StartTime));
247  }
248  }
249  $MatchList = $MatchList + $this->DB->FetchColumn("ResourceId");
250  }
251  }
252  else
253  {
254  $MatchList = array();
255  }
256 
257  # return list of matching resources to caller
258  return $MatchList;
259  }
260 
270  $FieldIds, $Operators, $Values, $Logic)
271  {
272  # use SQL keyword appropriate to current search logic for combining operations
273  $CombineWord = ($Logic == "AND") ? " AND " : " OR ";
274 
275  # for each comparison
276  foreach ($FieldIds as $Index => $FieldId)
277  {
278  $Operator = $Operators[$Index];
279  $Value = $Values[$Index];
280 
282  {
283  $Field = new MetadataField($FieldId);
284  switch ($Field->Type())
285  {
291  if (isset($Queries["Resources"]))
292  {
293  $Queries["Resources"] .= $CombineWord;
294  }
295  else
296  {
297  $Queries["Resources"] = "SELECT DISTINCT ResourceId"
298  ." FROM Resources WHERE ";
299  }
300  $Queries["Resources"] .= $this->GetTextComparisonSql(
301  $Field->DBFieldName(), $Operator, $Value);
302  break;
303 
305  $QueryIndex = "ResourceUserInts".$FieldId;
306 
307  if (isset($Queries[$QueryIndex]))
308  {
309  $Queries[$QueryIndex] .= $CombineWord;
310  }
311  else
312  {
313  $Queries[$QueryIndex] = "SELECT DISTINCT ResourceId"
314  ." FROM ResourceUserInts WHERE ";
315  }
316 
317  $User = new CWUser($Value);
318 
319  $Queries[$QueryIndex] .=
320  "(UserId = ".$User->Id()." AND FieldId = "
321  .intval($FieldId).")";
322  break;
323 
325  $QueryIndex = "ResourceNameInts".$Field->Id();
326  if (!isset($Queries[$QueryIndex]["A"]))
327  {
328  $Queries[$QueryIndex]["A"] =
329  "SELECT DISTINCT ResourceId"
330  ." FROM ResourceNameInts, ControlledNames "
331  ." WHERE ControlledNames.FieldId = "
332  .intval($FieldId)
333  ." AND ( ";
334  $CloseQuery[$QueryIndex]["A"] = TRUE;
335  $ComparisonCount[$QueryIndex]["A"] = 1;
336  $ComparisonCountField[$QueryIndex]["A"] = "ControlledName";
337  }
338  else
339  {
340  $Queries[$QueryIndex]["A"] .= " OR ";
341  $ComparisonCount[$QueryIndex]["A"]++;
342  }
343  $Queries[$QueryIndex]["A"] .=
344  "(ResourceNameInts.ControlledNameId"
345  ." = ControlledNames.ControlledNameId"
346  ." AND ".$this->GetTextComparisonSql(
347  "ControlledName", $Operator, $Value)
348  .")";
349  if (!isset($Queries[$QueryIndex]["B"]))
350  {
351  $Queries[$QueryIndex]["B"] =
352  "SELECT DISTINCT ResourceId"
353  . " FROM ResourceNameInts, ControlledNames,"
354  ." VariantNames "
355  ." WHERE ControlledNames.FieldId = "
356  .intval($Field->Id())
357  ." AND ( ";
358  $CloseQuery[$QueryIndex]["B"] = TRUE;
359  $ComparisonCount[$QueryIndex]["B"] = 1;
360  $ComparisonCountField[$QueryIndex]["B"] = "ControlledName";
361  }
362  else
363  {
364  $Queries[$QueryIndex]["B"] .= " OR ";
365  $ComparisonCount[$QueryIndex]["B"]++;
366  }
367  $Queries[$QueryIndex]["B"] .=
368  "(ResourceNameInts.ControlledNameId"
369  ." = ControlledNames.ControlledNameId"
370  ." AND ResourceNameInts.ControlledNameId"
371  ." = VariantNames.ControlledNameId"
372  ." AND ".$this->GetTextComparisonSql(
373  "VariantName", $Operator, $Value)
374  .")";
375  break;
376 
378  $QueryIndex = "ResourceNameInts".$FieldId;
379  if (!isset($Queries[$QueryIndex]))
380  {
381  $Queries[$QueryIndex] =
382  "SELECT DISTINCT ResourceId"
383  ." FROM ResourceNameInts, ControlledNames "
384  ." WHERE ControlledNames.FieldId = "
385  .intval($FieldId)
386  ." AND ( ";
387  $CloseQuery[$QueryIndex] = TRUE;
388  $ComparisonCount[$QueryIndex] = 1;
389  $ComparisonCountField[$QueryIndex] = "ControlledName";
390  }
391  else
392  {
393  $Queries[$QueryIndex] .= " OR ";
394  $ComparisonCount[$QueryIndex]++;
395  }
396  $Queries[$QueryIndex] .=
397  "(ResourceNameInts.ControlledNameId"
398  ." = ControlledNames.ControlledNameId"
399  ." AND ".$this->GetTextComparisonSql(
400  "ControlledName", $Operator, $Value)
401  .")";
402  break;
403 
405  $QueryIndex = "ResourceClassInts".$FieldId;
406  if (!isset($Queries[$QueryIndex]))
407  {
408  $Queries[$QueryIndex] = "SELECT DISTINCT ResourceId"
409  ." FROM ResourceClassInts, Classifications"
410  ." WHERE ResourceClassInts.ClassificationId"
411  ." = Classifications.ClassificationId"
412  ." AND Classifications.FieldId"
413  ." = ".intval($FieldId)." AND ( ";
414  $CloseQuery[$QueryIndex] = TRUE;
415  $ComparisonCount[$QueryIndex] = 1;
416  $ComparisonCountField[$QueryIndex] = "ClassificationName";
417  }
418  else
419  {
420  $Queries[$QueryIndex] .= " OR ";
421  $ComparisonCount[$QueryIndex]++;
422  }
423  $Queries[$QueryIndex] .= $this->GetTextComparisonSql(
424  "ClassificationName", $Operator, $Value);
425  break;
426 
428  # if value appears to have time component or text description
429  if (strpos($Value, ":")
430  || strstr($Value, "day")
431  || strstr($Value, "week")
432  || strstr($Value, "month")
433  || strstr($Value, "year")
434  || strstr($Value, "hour")
435  || strstr($Value, "minute"))
436  {
437  if (isset($Queries["Resources"]))
438  {
439  $Queries["Resources"] .= $CombineWord;
440  }
441  else
442  {
443  $Queries["Resources"] = "SELECT DISTINCT ResourceId"
444  ." FROM Resources WHERE ";
445  }
446 
447  # flip operator if necessary
448  if (strstr($Value, "ago"))
449  {
450  $OperatorFlipMap = array(
451  "<" => ">=",
452  ">" => "<=",
453  "<=" => ">",
454  ">=" => "<",
455  );
456  $Operator = isset($OperatorFlipMap[$Operator])
457  ? $OperatorFlipMap[$Operator] : $Operator;
458  }
459 
460  # use strtotime method to build condition
461  $TimestampValue = strtotime($Value);
462  if (($TimestampValue !== FALSE) && ($TimestampValue != -1))
463  {
464  if ((date("H:i:s", $TimestampValue) == "00:00:00")
465  && (strpos($Value, "00:00") === FALSE)
466  && ($Operator == "<="))
467  {
468  $NormalizedValue =
469  date("Y-m-d", $TimestampValue)." 23:59:59";
470  }
471  else
472  {
473  $NormalizedValue = date(
474  "Y-m-d H:i:s", $TimestampValue);
475  }
476  }
477  else
478  {
479  $NormalizedValue = addslashes($Value);
480  }
481  $Queries["Resources"] .=
482  " ( `".$Field->DBFieldName()."` "
483  .$Operator
484  ." '".$NormalizedValue."' ) ";
485  }
486  else
487  {
488  # use Date object method to build condition
489  $Date = new Date($Value);
490  if ($Date->Precision())
491  {
492  if (isset($Queries["Resources"]))
493  {
494  $Queries["Resources"] .= $CombineWord;
495  }
496  else
497  {
498  $Queries["Resources"] = "SELECT DISTINCT ResourceId"
499  ." FROM Resources WHERE ";
500  }
501  $Queries["Resources"] .= " ( ".$Date->SqlCondition(
502  $Field->DBFieldName(), NULL, $Operator)." ) ";
503  }
504  }
505  break;
506 
508  $Date = new Date($Value);
509  if ($Date->Precision())
510  {
511  if (isset($Queries["Resources"]))
512  {
513  $Queries["Resources"] .= $CombineWord;
514  }
515  else
516  {
517  $Queries["Resources"] = "SELECT DISTINCT ResourceId"
518  ." FROM Resources WHERE ";
519  }
520  $Queries["Resources"] .= " ( ".$Date->SqlCondition(
521  $Field->DBFieldName()."Begin",
522  $Field->DBFieldName()."End", $Operator)." ) ";
523  }
524  break;
525 
527  $QueryIndex = "ReferenceInts".$FieldId;
528  if (!isset($Queries[$QueryIndex]))
529  {
530  $Queries[$QueryIndex] =
531  "SELECT DISTINCT RI.SrcResourceId AS ResourceId"
532  ." FROM ReferenceInts AS RI, Resources AS R "
533  ." WHERE RI.FieldId = ".intval($FieldId)
534  ." AND (";
535  $CloseQuery[$QueryIndex] = TRUE;
536  }
537  else
538  {
539  $Queries[$QueryIndex] .= $CombineWord;
540  }
541 
542  # iterate over all the schemas this field can reference,
543  # gluing together an array of subqueries for the mapped
544  # title field of each as we go
545  $SchemaIds = $Field->ReferenceableSchemaIds();
546 
547  # if no referenceable schemas configured, fall back to
548  # searching all schemas
549  if (count($SchemaIds)==0)
550  {
551  $SchemaIds = MetadataSchema::GetAllSchemaIds();
552  }
553 
554  $Subqueries = array();
555  foreach ($SchemaIds as $SchemaId)
556  {
557  $Schema = new MetadataSchema($SchemaId);
558  $MappedTitle = $Schema->GetFieldByMappedName("Title");
559 
560  $Subqueries[]= $this->GetTextComparisonSql(
561  $MappedTitle->DBFieldName(), $Operator, $Value, "R");
562  }
563 
564  # OR together all the subqueries, add it to the query
565  # for our field
566  $Queries[$QueryIndex] .=
567  "((".implode(" OR ", $Subqueries).")"
568  ." AND R.ResourceId = RI.DstResourceId)";
569  break;
570 
573  # (these types not yet handled by search engine for comparisons)
574  break;
575  }
576  }
577  }
578 
579  # if queries found
580  if (isset($Queries))
581  {
582  # for each assembled query
583  foreach ($Queries as $QueryIndex => $Query)
584  {
585  # if query has multiple parts
586  if (is_array($Query))
587  {
588  # for each part of query
589  $ResourceIds = array();
590  foreach ($Query as $PartIndex => $PartQuery)
591  {
592  # add closing paren if query was flagged to be closed
593  if (isset($CloseQuery[$QueryIndex][$PartIndex]))
594  {
595  $PartQuery .= ") ";
596  if (($Logic == "AND")
597  && ($ComparisonCount[$QueryIndex][$PartIndex] > 1))
598  {
599  $PartQuery .= "GROUP BY ResourceId"
600  ." HAVING COUNT(DISTINCT "
601  .$ComparisonCountField[$QueryIndex][$PartIndex]
602  .") = "
603  .$ComparisonCount[$QueryIndex][$PartIndex];
604  }
605  }
606 
607  # perform query and retrieve IDs
608  $this->DMsg(5, "Performing comparison query <i>"
609  .$PartQuery."</i>");
610  $this->DB->Query($PartQuery);
611  $ResourceIds = $ResourceIds
612  + $this->DB->FetchColumn("ResourceId");
613  $this->DMsg(5, "Comparison query produced <i>"
614  .count($ResourceIds)."</i> results");
615  }
616  }
617  else
618  {
619  # add closing paren if query was flagged to be closed
620  if (isset($CloseQuery[$QueryIndex]))
621  {
622  $Query .= ") ";
623  if (($Logic == "Logic")
624  && ($ComparisonCount[$QueryIndex] > 1))
625  {
626  $Query .= "GROUP BY ResourceId"
627  ." HAVING COUNT(DISTINCT "
628  .$ComparisonCountField[$QueryIndex]
629  .") = "
630  .$ComparisonCount[$QueryIndex];
631  }
632  }
633 
634  # perform query and retrieve IDs
635  $this->DMsg(5, "Performing comparison query <i>".$Query."</i>");
636  $this->DB->Query($Query);
637  $ResourceIds = $this->DB->FetchColumn("ResourceId");
638  $this->DMsg(5, "Comparison query produced <i>"
639  .count($ResourceIds)."</i> results");
640  }
641 
642  # if we already have some results
643  if (isset($Results))
644  {
645  # if search logic is set to AND
646  if ($Logic == "AND")
647  {
648  # remove anything from results that was not returned from query
649  $Results = array_intersect($Results, $ResourceIds);
650  }
651  else
652  {
653  # add values returned from query to results
654  $Results = array_unique(array_merge($Results, $ResourceIds));
655  }
656  }
657  else
658  {
659  # set results to values returned from query
660  $Results = $ResourceIds;
661  }
662  }
663  }
664  else
665  {
666  # initialize results to empty list
667  $Results = array();
668  }
669 
670  # return results to caller
671  return $Results;
672  }
673 
682  public static function GetItemIdsSortedByField(
683  $ItemType, $FieldId, $SortDescending)
684  {
685  $RFactory = new ResourceFactory($ItemType);
686  return $RFactory->GetResourceIdsSortedBy($FieldId, !$SortDescending);
687  }
688 
695  public static function QueueUpdateForItem($ItemOrItemId, $TaskPriority = NULL)
696  {
697  if (is_numeric($ItemOrItemId))
698  {
699  $ItemId = $ItemOrItemId;
700  $Item = new Resource($ItemId);
701  }
702  else
703  {
704  $Item = $ItemOrItemId;
705  $ItemId = $Item->Id();
706  }
707 
708  # if no priority was provided, use the default
709  if ($TaskPriority === NULL)
710  {
711  $TaskPriority = self::$TaskPriority;
712  }
713 
714  # assemble task description
715  $Title = $Item->GetMapped("Title");
716  if (!strlen($Title))
717  {
718  $Title = "Item #".$ItemId;
719  }
720  $TaskDescription = "Update search data for"
721  ." <a href=\"r".$ItemId."\"><i>"
722  .$Title."</i></a>";
723 
724  # queue update
725  $GLOBALS["AF"]->QueueUniqueTask(array(__CLASS__, "RunUpdateForItem"),
726  array(intval($ItemId)), $TaskPriority, $TaskDescription);
727  }
728 
733  public static function RunUpdateForItem($ItemId)
734  {
735  # bail out if item no longer exists
736  $Resource = new Resource($ItemId);
737  if ($Resource->Status() == -1) { return; }
738 
739  # bail out if item is a temporary record
740  if ($Resource->IsTempResource()) { return; }
741 
742  # retrieve schema ID of item to use for item type
743  $ItemType = $Resource->SchemaId();
744 
745  # update search data for resource
746  $SearchEngine = new SPTSearchEngine();
747  $SearchEngine->UpdateForItem($ItemId, $ItemType);
748  }
749 
758  public static function GetResultFacets($SearchResults, $User)
759  {
760  # classifications and names associated with these search results
761  $SearchClasses = array();
762  $SearchNames = array();
763 
764  # make sure we're not faceting too many resources
765  $SearchResults = array_slice(
766  $SearchResults, 0,
767  self::$NumResourcesForFacets,
768  TRUE);
769 
770  # disable DB cache for the search suggestions process,
771  # this avoids memory exhaustion.
772  $DB = new Database();
773  $DB->Caching(FALSE);
774 
775  # number of resources to include in a chunk
776  # a mysql BIGINT is at most 21 characters long and the
777  # default max_packet_size is 1 MiB, so we can pack about
778  # 1 MiB / (22 bytes) = 47,663 ResourceIds into a query before
779  # we need to worry about length problems
780  $ChunkSize = 47600;
781 
782  if (count($SearchResults)>0)
783  {
784  foreach (array_chunk($SearchResults, $ChunkSize, TRUE) as $Chunk)
785  {
786  # pull out all the Classifications that were associated
787  # with our search results along with all their parents
788  $DB->Query("SELECT ResourceId,ClassificationId FROM ResourceClassInts "
789  ."WHERE ResourceId IN "
790  ."(".implode(",", array_keys($Chunk)).")");
791  $Rows = $DB->FetchRows();
792  foreach ($Rows as $Row)
793  {
794  $CurId = $Row["ClassificationId"];
795  while ($CurId !== FALSE)
796  {
797  $SearchClasses[$CurId][]=$Row["ResourceId"] ;
798  $CurId = self::FindParentClass($CurId);
799  }
800  }
801 
802  # also pull out controlled names
803  $DB->Query("SELECT ResourceId,ControlledNameId FROM ResourceNameInts "
804  ."WHERE ResourceId in "
805  ."(".implode(",", array_keys($Chunk)).")");
806  $Rows = $DB->FetchRows();
807  foreach ($Rows as $Row)
808  {
809  $SearchNames[$Row["ControlledNameId"]][]= $Row["ResourceId"];
810  }
811  }
812 
813  # make sure we haven't double-counted resources that have
814  # a classification and some of its children assigned
815  $TmpClasses = array();
816  foreach ($SearchClasses as $ClassId => $Resources)
817  {
818  $TmpClasses[$ClassId] = array_unique($Resources);
819  }
820  $SearchClasses = $TmpClasses;
821  }
822 
823  # generate a map of FieldId -> Field Names for all of the generated facets:
824  $SuggestionsById = array();
825 
826  # pull relevant Classification names out of the DB
827  if (count($SearchClasses)>0)
828  {
829  foreach (array_chunk($SearchClasses, $ChunkSize, TRUE) as $Chunk)
830  {
831  $DB->Query("SELECT FieldId,ClassificationId,ClassificationName"
832  ." FROM Classifications"
833  ." WHERE ClassificationId"
834  ." IN (".implode(",", array_keys($Chunk)).")");
835  foreach ($DB->FetchRows() as $Row)
836  {
837  $SuggestionsById[$Row["FieldId"]][]=
838  array("Id" => $Row["ClassificationId"],
839  "Name" => $Row["ClassificationName"],
840  "Count" => count(
841  $SearchClasses[$Row["ClassificationId"]]));
842  }
843  }
844  }
845 
846  # pull relevant ControlledNames out of the DB
847  if (count($SearchNames)>0)
848  {
849  foreach (array_chunk($SearchNames, $ChunkSize, TRUE) as $Chunk)
850  {
851  $DB->Query("SELECT FieldId,ControlledNameId,ControlledName"
852  ." FROM ControlledNames"
853  ." WHERE ControlledNameId"
854  ." IN (".implode(",", array_keys($SearchNames)).")");
855  foreach ($DB->FetchRows() as $Row)
856  {
857  $SuggestionsById[$Row["FieldId"]][]=
858  array("Id" => $Row["ControlledNameId"],
859  "Name" => $Row["ControlledName"],
860  "Count" => count(
861  $SearchNames[$Row["ControlledNameId"]]));
862  }
863  }
864  }
865 
866  # translate the suggestions that we have in terms of the
867  # FieldIds to suggestions in terms of the field names
868  $SuggestionsByFieldName = array();
869 
870  # if we have suggestions to offer
871  if (count($SuggestionsById)>0)
872  {
873  # gill in an array that maps FieldNames to search links
874  # which would be appropriate for that field
875  foreach ($SuggestionsById as $FieldId => $FieldValues)
876  {
877  try
878  {
879  $ThisField = new MetadataField($FieldId);
880  }
881  catch (Exception $Exception)
882  {
883  $ThisField = NULL;
884  }
885 
886  # bail on fields that didn't exist and on fields that the
887  # current user cannot view, and on fields that are
888  # disabled for advanced searching
889  if (is_object($ThisField) &&
890  $ThisField->Status() == MetadataSchema::MDFSTAT_OK &&
891  $ThisField->IncludeInFacetedSearch() &&
892  $ThisField->Enabled() &&
893  $User->HasPriv($ThisField->ViewingPrivileges()))
894  {
895  $SuggestionsByFieldName[$ThisField->Name()] = array();
896 
897  foreach ($FieldValues as $Value)
898  {
899  $SuggestionsByFieldName[$ThisField->Name()][$Value["Id"]] =
900  array("Name" => $Value["Name"], "Count" => $Value["Count"] );
901  }
902  }
903  }
904  }
905 
906  ksort($SuggestionsByFieldName);
907 
908  return $SuggestionsByFieldName;
909  }
910 
915  public function UpdateForResource($ItemId)
916  {
917  $this->UpdateForItem($ItemId);
918  }
919 
925  public static function SetUpdatePriority($NewPriority)
926  {
927  self::$TaskPriority = $NewPriority;
928  }
929 
934  public static function SetNumResourcesForFacets($NumToUse)
935  {
936  self::$NumResourcesForFacets = $NumToUse;
937  }
938 
939  # ---- BACKWARD COMPATIBILITY --------------------------------------------
940 
960  public function GroupedSearch(
961  $SearchGroups, $StartingResult = 0, $NumberOfResults = 10,
962  $SortByField = NULL, $SortDescending = TRUE)
963  {
964  if ($SearchGroups instanceof SearchParameterSet)
965  {
966  # if search parameter set was passed in, use it directly
967  $SearchParams = $SearchGroups;
968  }
969  else
970  {
971  # otherwise, convert legacy array into SearchParameterSet
972  $SearchParams = new SearchParameterSet();
973  $SearchParams->SetFromLegacyArray($SearchGroups);
974  }
975 
976  # perform search
977  $Results = $this->Search(
978  $SearchParams, $StartingResult, $NumberOfResults,
979  $SortByField, $SortDescending);
980 
981  # pull out the resoults for the Resource schema
982  if (isset($Results[MetadataSchema::SCHEMAID_DEFAULT]))
983  {
984  $Results = $Results[MetadataSchema::SCHEMAID_DEFAULT];
985  }
986  else
987  {
988  $Results = array();
989  }
990 
991  # return the results
992  return $Results;
993  }
994 
1000  public static function ConvertToDisplayParameters($SearchParams)
1001  {
1002  # create display parameters, used to make a more user-friendly
1003  # version of the search
1004  $DisplayParams = new SearchParameterSet();
1005 
1006  # copy keyword searches as is
1007  $DisplayParams->AddParameter(
1008  $SearchParams->GetKeywordSearchStrings() );
1009 
1010  # copy field searches as is
1011  $SearchStrings = $SearchParams->GetSearchStrings();
1012  foreach ($SearchStrings as $FieldId => $Params)
1013  {
1014  $DisplayParams->AddParameter($Params, $FieldId);
1015  }
1016 
1017  # iterate over the search groups, looking for the 'is or begins
1018  # with' group that we add when faceting
1019  $Groups = $SearchParams->GetSubgroups();
1020  foreach ($Groups as $Group)
1021  {
1022  # if this group uses OR logic for a single field
1023  if ($Group->Logic() == "OR" &&
1024  count($Group->GetFields()) == 1)
1025  {
1026  # pull out the search strings for this field
1027  $SearchStrings = $Group->GetSearchStrings();
1028  $FieldId = key($SearchStrings);
1029  $Values = current($SearchStrings);
1030 
1031  # check if this field is valid anywhere
1033  {
1034  $Field = new MetadataField($FieldId);
1035  # if this is a tree field, and we're doing a prefix match
1036  # on it, we'll just want to display this subgroup as an "IS"
1037  # otherwise display the group unmodified
1038  if ($Field->Type() == MetadataSchema::MDFTYPE_TREE &&
1039  preg_match('/^=(.*)$/', $Values[0], $FirstMatch) &&
1040  preg_match('/^\\^(.*) -- $/', $Values[1], $SecondMatch) &&
1041  $FirstMatch[1] == $SecondMatch[1] )
1042  {
1043  $DisplayParams->AddParameter("=".$FirstMatch[1], $FieldId);
1044  }
1045  else
1046  {
1047  $DisplayParams->AddSet($Group);
1048  }
1049  }
1050  }
1051  else
1052  {
1053  # otherwise, just attempt to include the group as-is
1054  try
1055  {
1056  $DisplayParams->AddSet($Group);
1057  }
1058  catch (Exception $e)
1059  {
1060  # if group could not be added for any reason, continue
1061  }
1062  }
1063  }
1064 
1065  return $DisplayParams;
1066  }
1067 
1068  # ---- PRIVATE INTERFACE -------------------------------------------------
1069 
1070  private $FieldTypes;
1071  private $Schemas;
1072 
1073  private static $TaskPriority = ApplicationFramework::PRIORITY_BACKGROUND;
1074  private static $NumResourcesForFacets = 500;
1075 
1084  private function GetTextComparisonSql($DBField, $Operator, $Value, $Prefix="")
1085  {
1086  # if we were given a prefix, add the necessary period so we can use it
1087  if (strlen($Prefix))
1088  {
1089  $Prefix = $Prefix.".";
1090  }
1091 
1092  if ($Operator == "^")
1093  {
1094  $EscapedValue = str_replace(
1095  array("%", "_"),
1096  array("\%", "\_"),
1097  addslashes($Value));
1098  $Comparison = $Prefix."`".$DBField."` LIKE '".$EscapedValue."%' ";
1099  }
1100  elseif ($Operator == '$')
1101  {
1102  $EscapedValue = str_replace(
1103  array("%", "_"),
1104  array("\%", "\_"),
1105  addslashes($Value));
1106  $Comparison = $Prefix."`".$DBField."` LIKE '%".$EscapedValue."' ";
1107  }
1108  elseif ($Operator == '!=')
1109  {
1110  $Comparison =
1111  "(".$Prefix."`".$DBField."` ".$Operator." '".addslashes($Value)."'"
1112  ." AND ".$Prefix."`".$DBField."` IS NOT NULL)";
1113  }
1114  else
1115  {
1116  $Comparison = $Prefix."`".$DBField."` "
1117  .$Operator." '".addslashes($Value)."' ";
1118  }
1119  return $Comparison;
1120  }
1121 
1129  private static function FindParentClass($ClassId)
1130  {
1131  static $ParentMap;
1132 
1133  # first time through, fetch the mapping of parent values we need
1134  if (!isset($ParentMap))
1135  {
1136  $DB = new Database();
1137 
1138  # result here will be a parent/child mapping for all used
1139  # classifications; avoid caching it as it can be quite large
1140  $PreviousSetting = $DB->Caching();
1141  $DB->Caching(FALSE);
1142  $DB->Query(
1143  "SELECT ParentId, ClassificationId FROM Classifications "
1144  ."WHERE DEPTH > 0 AND FullResourceCount > 0 "
1145  ."AND FieldId IN (SELECT FieldId FROM MetadataFields "
1146  ." WHERE IncludeInFacetedSearch=1)"
1147  );
1148  $DB->Caching($PreviousSetting);
1149 
1150  $ParentMap = $DB->FetchColumn("ParentId", "ClassificationId");
1151  }
1152 
1153  return isset($ParentMap[$ClassId]) ? $ParentMap[$ClassId] : FALSE;
1154  }
1155 }
AddField($FieldId, $FieldType, $ItemTypes, $Weight, $UsedInKeywordSearch)
Add field to include in searching.
Metadata schema (in effect a Factory class for MetadataField).
Set of parameters used to perform a search.
static SetUpdatePriority($NewPriority)
Set the default priority for background tasks.
SQL database abstraction object with smart query caching.
Definition: Database.php:22
static SetNumResourcesForFacets($NumToUse)
Set the number of resources used for search facets.
UpdateForResource($ItemId)
Compat function for old SPT code to update resources.
UpdateForItem($ItemId, $ItemType)
Update search database for the specified item.
Definition: Date.php:18
const MDFTYPE_CONTROLLEDNAME
static GetItemIdsSortedByField($ItemType, $FieldId, $SortDescending)
Return item IDs sorted by a specified field.
static GetAllSchemaIds()
Get IDs for all existing metadata schemas.
static RunUpdateForItem($ItemId)
Update search index for an item.
static FieldExistsInAnySchema($Field)
Determine if a Field exists in any schema.
GroupedSearch($SearchGroups, $StartingResult=0, $NumberOfResults=10, $SortByField=NULL, $SortDescending=TRUE)
Perform search with logical groups of fielded searches.
SearchFieldForPhrases($FieldId, $Phrase)
Perform phrase searching.
Search($SearchParams, $StartingResult=0, $NumberOfResults=PHP_INT_MAX, $SortByField=NULL, $SortDescending=TRUE)
Perform search with specified parameters.
Object representing a locally-defined type of metadata field.
DMsg($Level, $Msg)
Print debug message if level set high enough.
Represents a "resource" in CWIS.
Definition: Resource.php:13
static GetResultFacets($SearchResults, $User)
Generate a list of suggested additional search terms that can be used for faceted searching...
GetFieldContent($ItemId, $FieldId)
Overloaded version of method to retrieve text from DB.
Core metadata archive search engine class.
static ConvertToDisplayParameters($SearchParams)
Get a simplified SearchParameterSet for display purposes.
SearchFieldsForComparisonMatches($FieldIds, $Operators, $Values, $Logic)
Perform comparison searches.
DebugLevel($NewValue)
Set debug output level.
Factory for Resource objects.
CWIS-specific user class.
Definition: CWUser.php:13
static GetAllSchemas()
Get all existing metadata schemas.
__construct()
Class constructor.
static QueueUpdateForItem($ItemOrItemId, $TaskPriority=NULL)
Queue background update for an item.