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-2016 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  # skip field if it is not valid
280  {
281  continue;
282  }
283 
284  $Field = new MetadataField($FieldId);
285  $Operator = $Operators[$Index];
286  $Value = $Values[$Index];
287 
288  $ProcessingType = ($Operator{0} == "@")
289  ? "Modification Comparison" : $Field->Type();
290  switch ($ProcessingType)
291  {
297  $QueryConditions["Resources"][] = $this->GetTextComparisonSql(
298  $Field->DBFieldName(), $Operator, $Value);
299  break;
300 
302  $User = new CWUser($Value);
303  $QueryConditions["ResourceUserInts".$FieldId][] =
304  "(UserId = ".$User->Id()." AND FieldId = "
305  .intval($FieldId).")";
306  break;
307 
309  $QueryIndex = "ResourceNameInts".$FieldId;
310  if (!isset($Queries[$QueryIndex]["A"]))
311  {
312  $Queries[$QueryIndex]["A"] =
313  "SELECT DISTINCT ResourceId"
314  ." FROM ResourceNameInts, ControlledNames "
315  ." WHERE ControlledNames.FieldId = "
316  .intval($FieldId)
317  ." AND ( ";
318  $CloseQuery[$QueryIndex]["A"] = TRUE;
319  $ComparisonCount[$QueryIndex]["A"] = 1;
320  $ComparisonCountField[$QueryIndex]["A"] = "ControlledName";
321  }
322  else
323  {
324  $Queries[$QueryIndex]["A"] .= " OR ";
325  $ComparisonCount[$QueryIndex]["A"]++;
326  }
327  $Queries[$QueryIndex]["A"] .=
328  "(ResourceNameInts.ControlledNameId"
329  ." = ControlledNames.ControlledNameId"
330  ." AND ".$this->GetTextComparisonSql(
331  "ControlledName", $Operator, $Value)
332  .")";
333  if (!isset($Queries[$QueryIndex]["B"]))
334  {
335  $Queries[$QueryIndex]["B"] =
336  "SELECT DISTINCT ResourceId"
337  . " FROM ResourceNameInts, ControlledNames,"
338  ." VariantNames "
339  ." WHERE ControlledNames.FieldId = "
340  .intval($FieldId)
341  ." AND ( ";
342  $CloseQuery[$QueryIndex]["B"] = TRUE;
343  $ComparisonCount[$QueryIndex]["B"] = 1;
344  $ComparisonCountField[$QueryIndex]["B"] = "ControlledName";
345  }
346  else
347  {
348  $Queries[$QueryIndex]["B"] .= " OR ";
349  $ComparisonCount[$QueryIndex]["B"]++;
350  }
351  $Queries[$QueryIndex]["B"] .=
352  "(ResourceNameInts.ControlledNameId"
353  ." = ControlledNames.ControlledNameId"
354  ." AND ResourceNameInts.ControlledNameId"
355  ." = VariantNames.ControlledNameId"
356  ." AND ".$this->GetTextComparisonSql(
357  "VariantName", $Operator, $Value)
358  .")";
359  break;
360 
362  $QueryIndex = "ResourceNameInts".$FieldId;
363  if (!isset($Queries[$QueryIndex]))
364  {
365  $Queries[$QueryIndex] =
366  "SELECT DISTINCT ResourceId"
367  ." FROM ResourceNameInts, ControlledNames "
368  ." WHERE ControlledNames.FieldId = "
369  .intval($FieldId)
370  ." AND ( ";
371  $CloseQuery[$QueryIndex] = TRUE;
372  $ComparisonCount[$QueryIndex] = 1;
373  $ComparisonCountField[$QueryIndex] = "ControlledName";
374  }
375  else
376  {
377  $Queries[$QueryIndex] .= " OR ";
378  $ComparisonCount[$QueryIndex]++;
379  }
380  $Queries[$QueryIndex] .=
381  "(ResourceNameInts.ControlledNameId"
382  ." = ControlledNames.ControlledNameId"
383  ." AND ".$this->GetTextComparisonSql(
384  "ControlledName", $Operator, $Value)
385  .")";
386  break;
387 
389  $QueryIndex = "ResourceClassInts".$FieldId;
390  if (!isset($Queries[$QueryIndex]))
391  {
392  $Queries[$QueryIndex] = "SELECT DISTINCT ResourceId"
393  ." FROM ResourceClassInts, Classifications"
394  ." WHERE ResourceClassInts.ClassificationId"
395  ." = Classifications.ClassificationId"
396  ." AND Classifications.FieldId"
397  ." = ".intval($FieldId)." AND ( ";
398  $CloseQuery[$QueryIndex] = TRUE;
399  $ComparisonCount[$QueryIndex] = 1;
400  $ComparisonCountField[$QueryIndex] = "ClassificationName";
401  }
402  else
403  {
404  $Queries[$QueryIndex] .= " OR ";
405  $ComparisonCount[$QueryIndex]++;
406  }
407  $Queries[$QueryIndex] .= $this->GetTextComparisonSql(
408  "ClassificationName", $Operator, $Value);
409  break;
410 
412  # if we have an SQL conditional
413  $TimestampConditional = $this->GetTimeComparisonSql(
414  $Field, $Operator, $Value);
415  if ($TimestampConditional)
416  {
417  # add conditional
418  $QueryConditions["Resources"][] = $TimestampConditional;
419  }
420  break;
421 
423  $Date = new Date($Value);
424  if ($Date->Precision())
425  {
426  $QueryConditions["Resources"][] =
427  " ( ".$Date->SqlCondition(
428  $Field->DBFieldName()."Begin",
429  $Field->DBFieldName()."End", $Operator)." ) ";
430  }
431  break;
432 
434  $QueryIndex = "ReferenceInts".$FieldId;
435  if (!isset($Queries[$QueryIndex]))
436  {
437  $Queries[$QueryIndex] =
438  "SELECT DISTINCT RI.SrcResourceId AS ResourceId"
439  ." FROM ReferenceInts AS RI, Resources AS R "
440  ." WHERE RI.FieldId = ".intval($FieldId)
441  ." AND (";
442  $CloseQuery[$QueryIndex] = TRUE;
443  }
444  else
445  {
446  $Queries[$QueryIndex] .= $CombineWord;
447  }
448 
449  # iterate over all the schemas this field can reference,
450  # gluing together an array of subqueries for the mapped
451  # title field of each as we go
452  $SchemaIds = $Field->ReferenceableSchemaIds();
453 
454  # if no referenceable schemas configured, fall back to
455  # searching all schemas
456  if (count($SchemaIds)==0)
457  {
458  $SchemaIds = MetadataSchema::GetAllSchemaIds();
459  }
460 
461  $Subqueries = array();
462  foreach ($SchemaIds as $SchemaId)
463  {
464  $Schema = new MetadataSchema($SchemaId);
465  $MappedTitle = $Schema->GetFieldByMappedName("Title");
466 
467  $Subqueries[]= $this->GetTextComparisonSql(
468  $MappedTitle->DBFieldName(), $Operator, $Value, "R");
469  }
470 
471  # OR together all the subqueries, add it to the query
472  # for our field
473  $Queries[$QueryIndex] .=
474  "((".implode(" OR ", $Subqueries).")"
475  ." AND R.ResourceId = RI.DstResourceId)";
476  break;
477 
478  case "Modification Comparison":
479  # if we have an SQL conditional
480  $TimestampConditional = $this->GetTimeComparisonSql(
481  $Field, $Operator, $Value);
482  if ($TimestampConditional)
483  {
484  # add conditional
485  $QueryConditions["ResourceFieldTimestamps"][] =
486  $TimestampConditional;
487  }
488  break;
489 
490  default:
491  throw new Exception("Search of unknown field type ("
492  .$ProcessingType.").");
493  break;
494  }
495  }
496 
497  # if query conditions found
498  if (isset($QueryConditions))
499  {
500  # for each query condition group
501  foreach ($QueryConditions as $TargetTable => $Conditions)
502  {
503  # add entry with conditions to query list
504  if (isset($Queries[$TargetTable]))
505  {
506  $Queries[$TargetTable] .= $CombineWord
507  .implode($CombineWord, $Conditions);
508  }
509  else
510  {
511  $Queries[$TargetTable] = "SELECT DISTINCT ResourceId"
512  ." FROM ".$TargetTable." WHERE "
513  .implode($CombineWord, $Conditions);
514  }
515  }
516  }
517 
518  # if queries found
519  if (isset($Queries))
520  {
521  # for each assembled query
522  foreach ($Queries as $QueryIndex => $Query)
523  {
524  # if query has multiple parts
525  if (is_array($Query))
526  {
527  # for each part of query
528  $ResourceIds = array();
529  foreach ($Query as $PartIndex => $PartQuery)
530  {
531  # add closing paren if query was flagged to be closed
532  if (isset($CloseQuery[$QueryIndex][$PartIndex]))
533  {
534  $PartQuery .= ") ";
535  if (($Logic == "AND")
536  && ($ComparisonCount[$QueryIndex][$PartIndex] > 1))
537  {
538  $PartQuery .= "GROUP BY ResourceId"
539  ." HAVING COUNT(DISTINCT "
540  .$ComparisonCountField[$QueryIndex][$PartIndex]
541  .") = "
542  .$ComparisonCount[$QueryIndex][$PartIndex];
543  }
544  }
545 
546  # perform query and retrieve IDs
547  $this->DMsg(5, "Performing comparison query <i>"
548  .$PartQuery."</i>");
549  $this->DB->Query($PartQuery);
550  $ResourceIds = $ResourceIds
551  + $this->DB->FetchColumn("ResourceId");
552  $this->DMsg(5, "Comparison query produced <i>"
553  .count($ResourceIds)."</i> results");
554  }
555  }
556  else
557  {
558  # add closing paren if query was flagged to be closed
559  if (isset($CloseQuery[$QueryIndex]))
560  {
561  $Query .= ") ";
562  if (($Logic == "Logic")
563  && ($ComparisonCount[$QueryIndex] > 1))
564  {
565  $Query .= "GROUP BY ResourceId"
566  ." HAVING COUNT(DISTINCT "
567  .$ComparisonCountField[$QueryIndex]
568  .") = "
569  .$ComparisonCount[$QueryIndex];
570  }
571  }
572 
573  # perform query and retrieve IDs
574  $this->DMsg(5, "Performing comparison query <i>".$Query."</i>");
575  $this->DB->Query($Query);
576  $ResourceIds = $this->DB->FetchColumn("ResourceId");
577  $this->DMsg(5, "Comparison query produced <i>"
578  .count($ResourceIds)."</i> results");
579  }
580 
581  # if we already have some results
582  if (isset($Results))
583  {
584  # if search logic is set to AND
585  if ($Logic == "AND")
586  {
587  # remove anything from results that was not returned from query
588  $Results = array_intersect($Results, $ResourceIds);
589  }
590  else
591  {
592  # add values returned from query to results
593  $Results = array_unique(array_merge($Results, $ResourceIds));
594  }
595  }
596  else
597  {
598  # set results to values returned from query
599  $Results = $ResourceIds;
600  }
601  }
602  }
603  else
604  {
605  # initialize results to empty list
606  $Results = array();
607  }
608 
609  # return results to caller
610  return $Results;
611  }
612 
621  public static function GetItemIdsSortedByField(
622  $ItemType, $FieldId, $SortDescending)
623  {
624  $RFactory = new ResourceFactory($ItemType);
625  return $RFactory->GetResourceIdsSortedBy($FieldId, !$SortDescending);
626  }
627 
634  public static function QueueUpdateForItem($ItemOrItemId, $TaskPriority = NULL)
635  {
636  if (is_numeric($ItemOrItemId))
637  {
638  $ItemId = $ItemOrItemId;
639  $Item = new Resource($ItemId);
640  }
641  else
642  {
643  $Item = $ItemOrItemId;
644  $ItemId = $Item->Id();
645  }
646 
647  # if no priority was provided, use the default
648  if ($TaskPriority === NULL)
649  {
650  $TaskPriority = self::$TaskPriority;
651  }
652 
653  # assemble task description
654  $Title = $Item->GetMapped("Title");
655  if (!strlen($Title))
656  {
657  $Title = "Item #".$ItemId;
658  }
659  $TaskDescription = "Update search data for"
660  ." <a href=\"r".$ItemId."\"><i>"
661  .$Title."</i></a>";
662 
663  # queue update
664  $GLOBALS["AF"]->QueueUniqueTask(array(__CLASS__, "RunUpdateForItem"),
665  array(intval($ItemId)), $TaskPriority, $TaskDescription);
666  }
667 
672  public static function RunUpdateForItem($ItemId)
673  {
674  # bail out if item no longer exists
675  $Resource = new Resource($ItemId);
676 
677  # bail out if item is a temporary record
678  if ($Resource->IsTempResource()) { return; }
679 
680  # retrieve schema ID of item to use for item type
681  $ItemType = $Resource->SchemaId();
682 
683  # update search data for resource
684  $SearchEngine = new SPTSearchEngine();
685  $SearchEngine->UpdateForItem($ItemId, $ItemType);
686  }
687 
696  public static function GetResultFacets($SearchResults, $User)
697  {
698  # classifications and names associated with these search results
699  $SearchClasses = array();
700  $SearchNames = array();
701 
702  # make sure we're not faceting too many resources
703  $SearchResults = array_slice(
704  $SearchResults, 0,
705  self::$NumResourcesForFacets,
706  TRUE);
707 
708  # disable DB cache for the search suggestions process,
709  # this avoids memory exhaustion.
710  $DB = new Database();
711  $DB->Caching(FALSE);
712 
713  # number of resources to include in a chunk
714  # a mysql BIGINT is at most 21 characters long and the
715  # default max_packet_size is 1 MiB, so we can pack about
716  # 1 MiB / (22 bytes) = 47,663 ResourceIds into a query before
717  # we need to worry about length problems
718  $ChunkSize = 47600;
719 
720  if (count($SearchResults)>0)
721  {
722  foreach (array_chunk($SearchResults, $ChunkSize, TRUE) as $Chunk)
723  {
724  # pull out all the Classifications that were associated
725  # with our search results along with all their parents
726  $DB->Query("SELECT ResourceId,ClassificationId FROM ResourceClassInts "
727  ."WHERE ResourceId IN "
728  ."(".implode(",", array_keys($Chunk)).")");
729  $Rows = $DB->FetchRows();
730  foreach ($Rows as $Row)
731  {
732  $CurId = $Row["ClassificationId"];
733  while ($CurId !== FALSE)
734  {
735  $SearchClasses[$CurId][]=$Row["ResourceId"] ;
736  $CurId = self::FindParentClass($CurId);
737  }
738  }
739 
740  # also pull out controlled names
741  $DB->Query("SELECT ResourceId,ControlledNameId FROM ResourceNameInts "
742  ."WHERE ResourceId in "
743  ."(".implode(",", array_keys($Chunk)).")");
744  $Rows = $DB->FetchRows();
745  foreach ($Rows as $Row)
746  {
747  $SearchNames[$Row["ControlledNameId"]][]= $Row["ResourceId"];
748  }
749  }
750 
751  # make sure we haven't double-counted resources that have
752  # a classification and some of its children assigned
753  $TmpClasses = array();
754  foreach ($SearchClasses as $ClassId => $Resources)
755  {
756  $TmpClasses[$ClassId] = array_unique($Resources);
757  }
758  $SearchClasses = $TmpClasses;
759  }
760 
761  # generate a map of FieldId -> Field Names for all of the generated facets:
762  $SuggestionsById = array();
763 
764  # pull relevant Classification names out of the DB
765  if (count($SearchClasses)>0)
766  {
767  foreach (array_chunk($SearchClasses, $ChunkSize, TRUE) as $Chunk)
768  {
769  $DB->Query("SELECT FieldId,ClassificationId,ClassificationName"
770  ." FROM Classifications"
771  ." WHERE ClassificationId"
772  ." IN (".implode(",", array_keys($Chunk)).")");
773  foreach ($DB->FetchRows() as $Row)
774  {
775  $SuggestionsById[$Row["FieldId"]][]=
776  array("Id" => $Row["ClassificationId"],
777  "Name" => $Row["ClassificationName"],
778  "Count" => count(
779  $SearchClasses[$Row["ClassificationId"]]));
780  }
781  }
782  }
783 
784  # pull relevant ControlledNames out of the DB
785  if (count($SearchNames)>0)
786  {
787  foreach (array_chunk($SearchNames, $ChunkSize, TRUE) as $Chunk)
788  {
789  $DB->Query("SELECT FieldId,ControlledNameId,ControlledName"
790  ." FROM ControlledNames"
791  ." WHERE ControlledNameId"
792  ." IN (".implode(",", array_keys($SearchNames)).")");
793  foreach ($DB->FetchRows() as $Row)
794  {
795  $SuggestionsById[$Row["FieldId"]][]=
796  array("Id" => $Row["ControlledNameId"],
797  "Name" => $Row["ControlledName"],
798  "Count" => count(
799  $SearchNames[$Row["ControlledNameId"]]));
800  }
801  }
802  }
803 
804  # translate the suggestions that we have in terms of the
805  # FieldIds to suggestions in terms of the field names
806  $SuggestionsByFieldName = array();
807 
808  # if we have suggestions to offer
809  if (count($SuggestionsById)>0)
810  {
811  # gill in an array that maps FieldNames to search links
812  # which would be appropriate for that field
813  foreach ($SuggestionsById as $FieldId => $FieldValues)
814  {
815  try
816  {
817  $ThisField = new MetadataField($FieldId);
818  }
819  catch (Exception $Exception)
820  {
821  $ThisField = NULL;
822  }
823 
824  # bail on fields that didn't exist and on fields that the
825  # current user cannot view, and on fields that are
826  # disabled for advanced searching
827  if (is_object($ThisField) &&
828  $ThisField->Status() == MetadataSchema::MDFSTAT_OK &&
829  $ThisField->IncludeInFacetedSearch() &&
830  $ThisField->Enabled() &&
831  $User->HasPriv($ThisField->ViewingPrivileges()))
832  {
833  $SuggestionsByFieldName[$ThisField->Name()] = array();
834 
835  foreach ($FieldValues as $Value)
836  {
837  $SuggestionsByFieldName[$ThisField->Name()][$Value["Id"]] =
838  array("Name" => $Value["Name"], "Count" => $Value["Count"] );
839  }
840  }
841  }
842  }
843 
844  ksort($SuggestionsByFieldName);
845 
846  return $SuggestionsByFieldName;
847  }
848 
854  public static function SetUpdatePriority($NewPriority)
855  {
856  self::$TaskPriority = $NewPriority;
857  }
858 
863  public static function SetNumResourcesForFacets($NumToUse)
864  {
865  self::$NumResourcesForFacets = $NumToUse;
866  }
867 
868  # ---- BACKWARD COMPATIBILITY --------------------------------------------
869 
889  public function GroupedSearch(
890  $SearchGroups, $StartingResult = 0, $NumberOfResults = 10,
891  $SortByField = NULL, $SortDescending = TRUE)
892  {
893  if ($SearchGroups instanceof SearchParameterSet)
894  {
895  # if search parameter set was passed in, use it directly
896  $SearchParams = $SearchGroups;
897  }
898  else
899  {
900  # otherwise, convert legacy array into SearchParameterSet
901  $SearchParams = new SearchParameterSet();
902  $SearchParams->SetFromLegacyArray($SearchGroups);
903  }
904 
905  # perform search
906  $Results = $this->Search(
907  $SearchParams, $StartingResult, $NumberOfResults,
908  $SortByField, $SortDescending);
909 
910  # pull out the resoults for the Resource schema
911  if (isset($Results[MetadataSchema::SCHEMAID_DEFAULT]))
912  {
913  $Results = $Results[MetadataSchema::SCHEMAID_DEFAULT];
914  }
915  else
916  {
917  $Results = array();
918  }
919 
920  # return the results
921  return $Results;
922  }
923 
943  public static function ConvertToDisplayParameters($SearchParams)
944  {
945  # create display parameters, used to make a more user-friendly
946  # version of the search
947  $DisplayParams = new SearchParameterSet();
948 
949  # copy keyword searches as is
950  $DisplayParams->AddParameter(
951  $SearchParams->GetKeywordSearchStrings() );
952 
953  # copy field searches as is
954  $SearchStrings = $SearchParams->GetSearchStrings();
955  foreach ($SearchStrings as $FieldId => $Params)
956  {
957  $DisplayParams->AddParameter($Params, $FieldId);
958  }
959 
960  # iterate over the search groups, looking for the 'is or begins
961  # with' group that we add when faceting and displaying them as
962  # IS parameters rather than the literal subgroup that we
963  # actually use
964  $Groups = $SearchParams->GetSubgroups();
965  foreach ($Groups as $Group)
966  {
967  # start off assuming that we'll just copy the group without conversion
968  $CopyGroup = TRUE;
969 
970  # if this group uses OR logic for a single field, then it
971  # might be one of the subgroups we want to match and will require further
972  # investigation
973  if ($Group->Logic() == "OR" &&
974  count($Group->GetFields()) == 1)
975  {
976  # pull out the search strings for this field
977  $SearchStrings = $Group->GetSearchStrings();
978  $FieldId = key($SearchStrings);
979  $Values = current($SearchStrings);
980 
981  # check if there are two search strings, one an 'is'
982  # and the other a 'begins with' that both start with the
983  # same prefix, as would be added by the search facet code
984  if ( count($Values) == 2 &&
985  preg_match('/^=(.*)$/', $Values[0], $FirstMatch) &&
986  preg_match('/^\\^(.*) -- $/', $Values[1], $SecondMatch) &&
987  $FirstMatch[1] == $SecondMatch[1] )
988  {
989  # check if this field is valid anywhere
991  {
992  $Field = new MetadataField($FieldId);
993 
994  # and check if this field is a tree field
995  if ($Field->Type() == MetadataSchema::MDFTYPE_TREE)
996  {
997  # okay, this group matches the form that
998  # the faceting code would add for an 'is or
999  # begins with' group; convert it to just an
1000  # 'is' group for display
1001  $DisplayParams->AddParameter("=".$FirstMatch[1], $FieldId);
1002  $CopyGroup = FALSE;
1003  }
1004  }
1005  }
1006  }
1007 
1008  # if this group didn't require conversion, attempt to copy
1009  # it verbatim
1010  if ($CopyGroup)
1011  {
1012  try
1013  {
1014  $DisplayParams->AddSet($Group);
1015  }
1016  catch (Exception $e)
1017  {
1018  # if group could not be added for any reason, skip
1019  # it and move on to the next group
1020  }
1021  }
1022  }
1023 
1024  return $DisplayParams;
1025  }
1026 
1027  # ---- PRIVATE INTERFACE -------------------------------------------------
1028 
1029  private $FieldTypes;
1030  private $Schemas;
1031 
1032  private static $TaskPriority = ApplicationFramework::PRIORITY_BACKGROUND;
1033  private static $NumResourcesForFacets = 500;
1034 
1043  private function GetTextComparisonSql($DBField, $Operator, $Value, $Prefix="")
1044  {
1045  # if we were given a prefix, add the necessary period so we can use it
1046  if (strlen($Prefix))
1047  {
1048  $Prefix = $Prefix.".";
1049  }
1050 
1051  if ($Operator == "^")
1052  {
1053  $EscapedValue = str_replace(
1054  array("%", "_"),
1055  array("\%", "\_"),
1056  addslashes($Value));
1057  $Comparison = $Prefix."`".$DBField."` LIKE '".$EscapedValue."%' ";
1058  }
1059  elseif ($Operator == '$')
1060  {
1061  $EscapedValue = str_replace(
1062  array("%", "_"),
1063  array("\%", "\_"),
1064  addslashes($Value));
1065  $Comparison = $Prefix."`".$DBField."` LIKE '%".$EscapedValue."' ";
1066  }
1067  elseif ($Operator == '!=')
1068  {
1069  $Comparison =
1070  "(".$Prefix."`".$DBField."` ".$Operator." '".addslashes($Value)."'"
1071  ." AND ".$Prefix."`".$DBField."` IS NOT NULL)";
1072  }
1073  else
1074  {
1075  $Comparison = $Prefix."`".$DBField."` "
1076  .$Operator." '".addslashes($Value)."' ";
1077  }
1078  return $Comparison;
1079  }
1080 
1089  private function GetTimeComparisonSql($Field, $Operator, $Value)
1090  {
1091  # check if this is a field modification comparison
1092  $ModificationComparison = ($Operator{0} == "@") ? TRUE : FALSE;
1093 
1094  # if value appears to have time component or text description
1095  if (strpos($Value, ":")
1096  || strstr($Value, "day")
1097  || strstr($Value, "week")
1098  || strstr($Value, "month")
1099  || strstr($Value, "year")
1100  || strstr($Value, "hour")
1101  || strstr($Value, "minute"))
1102  {
1103  # adjust operator if necessary
1104  if ($Operator == "@")
1105  {
1106  $Operator = ">=";
1107  }
1108  else
1109  {
1110  if ($ModificationComparison)
1111  {
1112  $Operator = substr($Operator, 1);
1113  }
1114 
1115  if (strstr($Value, "ago"))
1116  {
1117  $OperatorFlipMap = array(
1118  "<" => ">=",
1119  ">" => "<=",
1120  "<=" => ">",
1121  ">=" => "<",
1122  );
1123  $Operator = isset($OperatorFlipMap[$Operator])
1124  ? $OperatorFlipMap[$Operator] : $Operator;
1125  }
1126  }
1127 
1128  # translate common words-to-numbers
1129  $WordsForNumbers = array(
1130  '/^a /i' => '1 ',
1131  '/^an /i' => '1 ',
1132  '/^one /i' => '1 ',
1133  '/^two /i' => '2 ',
1134  '/^three /i' => '3 ',
1135  '/^four /i' => '4 ',
1136  '/^five /i' => '5 ',
1137  '/^six /i' => '6 ',
1138  '/^seven /i' => '7 ',
1139  '/^eight /i' => '8 ',
1140  '/^nine /i' => '9 ',
1141  '/^ten /i' => '10 ',
1142  '/^eleven /i' => '11 ',
1143  '/^twelve /i' => '12 ',
1144  '/^thirteen /i' => '13 ',
1145  '/^fourteen /i' => '14 ',
1146  '/^fifteen /i' => '15 ',
1147  '/^sixteen /i' => '16 ',
1148  '/^seventeen /i' => '17 ',
1149  '/^eighteen /i' => '18 ',
1150  '/^nineteen /i' => '19 ',
1151  '/^twenty /i' => '20 ',
1152  '/^thirty /i' => '30 ',
1153  '/^forty /i' => '40 ',
1154  '/^fourty /i' => '40 ', # (common misspelling)
1155  '/^fifty /i' => '50 ',
1156  '/^sixty /i' => '60 ',
1157  '/^seventy /i' => '70 ',
1158  '/^eighty /i' => '80 ',
1159  '/^ninety /i' => '90 ');
1160  $Value = preg_replace(
1161  array_keys($WordsForNumbers), $WordsForNumbers, $Value);
1162 
1163  # use strtotime method to build condition
1164  $TimestampValue = strtotime($Value);
1165  if (($TimestampValue !== FALSE) && ($TimestampValue != -1))
1166  {
1167  if ((date("H:i:s", $TimestampValue) == "00:00:00")
1168  && (strpos($Value, "00:00") === FALSE)
1169  && ($Operator == "<="))
1170  {
1171  $NormalizedValue =
1172  date("Y-m-d", $TimestampValue)." 23:59:59";
1173  }
1174  else
1175  {
1176  $NormalizedValue = date(
1177  "Y-m-d H:i:s", $TimestampValue);
1178  }
1179  }
1180  else
1181  {
1182  $NormalizedValue = addslashes($Value);
1183  }
1184 
1185  # build SQL conditional
1186  if ($ModificationComparison)
1187  {
1188  $Conditional = " ( FieldId = ".$Field->Id()
1189  ." AND Timestamp ".$Operator
1190  ." '".$NormalizedValue."' ) ";
1191  }
1192  else
1193  {
1194  $Conditional = " ( `".$Field->DBFieldName()."` "
1195  .$Operator." '".$NormalizedValue."' ) ";
1196  }
1197  }
1198  else
1199  {
1200  # adjust operator if necessary
1201  if ($ModificationComparison)
1202  {
1203  $Operator = ($Operator == "@") ? ">="
1204  : substr($Operator, 1);
1205  }
1206 
1207  # use Date object method to build conditional
1208  $Date = new Date($Value);
1209  if ($Date->Precision())
1210  {
1211  if ($ModificationComparison)
1212  {
1213  $Conditional = " ( FieldId = ".$Field->Id()
1214  ." AND ".$Date->SqlCondition(
1215  "Timestamp", NULL, $Operator)." ) ";
1216  }
1217  else
1218  {
1219  $Conditional = " ( ".$Date->SqlCondition(
1220  $Field->DBFieldName(), NULL, $Operator)." ) ";
1221  }
1222  }
1223  }
1224 
1225  # return assembled conditional to caller
1226  return $Conditional;
1227  }
1228 
1236  private static function FindParentClass($ClassId)
1237  {
1238  static $ParentMap;
1239 
1240  # first time through, fetch the mapping of parent values we need
1241  if (!isset($ParentMap))
1242  {
1243  $DB = new Database();
1244 
1245  # result here will be a parent/child mapping for all used
1246  # classifications; avoid caching it as it can be quite large
1247  $PreviousSetting = $DB->Caching();
1248  $DB->Caching(FALSE);
1249  $DB->Query(
1250  "SELECT ParentId, ClassificationId FROM Classifications "
1251  ."WHERE DEPTH > 0 AND FullResourceCount > 0 "
1252  ."AND FieldId IN (SELECT FieldId FROM MetadataFields "
1253  ." WHERE IncludeInFacetedSearch=1)"
1254  );
1255  $DB->Caching($PreviousSetting);
1256 
1257  $ParentMap = $DB->FetchColumn("ParentId", "ClassificationId");
1258  }
1259 
1260  return isset($ParentMap[$ClassId]) ? $ParentMap[$ClassId] : FALSE;
1261  }
1262 }
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.
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.
const PRIORITY_BACKGROUND
Lowest priority.
static QueueUpdateForItem($ItemOrItemId, $TaskPriority=NULL)
Queue background update for an item.