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"][] =
304  $this->GetUserComparisonSql(
305  $FieldId, $Operator, $User->Id());
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  if (is_numeric($Value))
450  {
451  # add subquery for specific resource ID
452  $Queries[$QueryIndex] .= "(RI.DstResourceId ".$Operator." '"
453  .addslashes($Value)."')";
454  }
455  else
456  {
457  # iterate over all the schemas this field can reference,
458  # gluing together an array of subqueries for the mapped
459  # title field of each as we go
460  $SchemaIds = $Field->ReferenceableSchemaIds();
461 
462  # if no referenceable schemas configured, fall back to
463  # searching all schemas
464  if (count($SchemaIds)==0)
465  {
466  $SchemaIds = MetadataSchema::GetAllSchemaIds();
467  }
468 
469  $Subqueries = array();
470  foreach ($SchemaIds as $SchemaId)
471  {
472  $Schema = new MetadataSchema($SchemaId);
473  $MappedTitle = $Schema->GetFieldByMappedName("Title");
474 
475  $Subqueries[]= $this->GetTextComparisonSql(
476  $MappedTitle->DBFieldName(), $Operator, $Value, "R");
477  }
478 
479  # OR together all the subqueries, add it to the query
480  # for our field
481  $Queries[$QueryIndex] .=
482  "((".implode(" OR ", $Subqueries).")"
483  ." AND R.ResourceId = RI.DstResourceId)";
484  }
485  break;
486 
487  case "Modification Comparison":
488  # if we have an SQL conditional
489  $TimestampConditional = $this->GetTimeComparisonSql(
490  $Field, $Operator, $Value);
491  if ($TimestampConditional)
492  {
493  # add conditional
494  $QueryConditions["ResourceFieldTimestamps"][] =
495  $TimestampConditional;
496  }
497  break;
498 
499  default:
500  throw new Exception("Search of unknown field type ("
501  .$ProcessingType.").");
502  break;
503  }
504  }
505 
506  # if query conditions found
507  if (isset($QueryConditions))
508  {
509  # for each query condition group
510  foreach ($QueryConditions as $TargetTable => $Conditions)
511  {
512  # add entry with conditions to query list
513  if (isset($Queries[$TargetTable]))
514  {
515  $Queries[$TargetTable] .= $CombineWord
516  .implode($CombineWord, $Conditions);
517  }
518  else
519  {
520  $Queries[$TargetTable] = "SELECT DISTINCT ResourceId"
521  ." FROM ".$TargetTable." WHERE "
522  .implode($CombineWord, $Conditions);
523  }
524  }
525  }
526 
527  # if queries found
528  if (isset($Queries))
529  {
530  # for each assembled query
531  foreach ($Queries as $QueryIndex => $Query)
532  {
533  # if query has multiple parts
534  if (is_array($Query))
535  {
536  # for each part of query
537  $ResourceIds = array();
538  foreach ($Query as $PartIndex => $PartQuery)
539  {
540  # add closing paren if query was flagged to be closed
541  if (isset($CloseQuery[$QueryIndex][$PartIndex]))
542  {
543  $PartQuery .= ") ";
544  if (($Logic == "AND")
545  && ($ComparisonCount[$QueryIndex][$PartIndex] > 1))
546  {
547  $PartQuery .= "GROUP BY ResourceId"
548  ." HAVING COUNT(DISTINCT "
549  .$ComparisonCountField[$QueryIndex][$PartIndex]
550  .") = "
551  .$ComparisonCount[$QueryIndex][$PartIndex];
552  }
553  }
554 
555  # perform query and retrieve IDs
556  $this->DMsg(5, "Performing comparison query <i>"
557  .$PartQuery."</i>");
558  $this->DB->Query($PartQuery);
559  $ResourceIds = $ResourceIds
560  + $this->DB->FetchColumn("ResourceId");
561  $this->DMsg(5, "Comparison query produced <i>"
562  .count($ResourceIds)."</i> results");
563  }
564  }
565  else
566  {
567  # add closing paren if query was flagged to be closed
568  if (isset($CloseQuery[$QueryIndex]))
569  {
570  $Query .= ") ";
571  if (($Logic == "Logic")
572  && ($ComparisonCount[$QueryIndex] > 1))
573  {
574  $Query .= "GROUP BY ResourceId"
575  ." HAVING COUNT(DISTINCT "
576  .$ComparisonCountField[$QueryIndex]
577  .") = "
578  .$ComparisonCount[$QueryIndex];
579  }
580  }
581 
582  # perform query and retrieve IDs
583  $this->DMsg(5, "Performing comparison query <i>".$Query."</i>");
584  $this->DB->Query($Query);
585  $ResourceIds = $this->DB->FetchColumn("ResourceId");
586  $this->DMsg(5, "Comparison query produced <i>"
587  .count($ResourceIds)."</i> results");
588  }
589 
590  # if we already have some results
591  if (isset($Results))
592  {
593  # if search logic is set to AND
594  if ($Logic == "AND")
595  {
596  # remove anything from results that was not returned from query
597  $Results = array_intersect($Results, $ResourceIds);
598  }
599  else
600  {
601  # add values returned from query to results
602  $Results = array_unique(array_merge($Results, $ResourceIds));
603  }
604  }
605  else
606  {
607  # set results to values returned from query
608  $Results = $ResourceIds;
609  }
610  }
611  }
612  else
613  {
614  # initialize results to empty list
615  $Results = array();
616  }
617 
618  # return results to caller
619  return $Results;
620  }
621 
630  public static function GetItemIdsSortedByField(
631  $ItemType, $FieldId, $SortDescending)
632  {
633  $RFactory = new ResourceFactory($ItemType);
634  return $RFactory->GetResourceIdsSortedBy($FieldId, !$SortDescending);
635  }
636 
643  public static function QueueUpdateForItem($ItemOrItemId, $TaskPriority = NULL)
644  {
645  if (is_numeric($ItemOrItemId))
646  {
647  $ItemId = $ItemOrItemId;
648  $Item = new Resource($ItemId);
649  }
650  else
651  {
652  $Item = $ItemOrItemId;
653  $ItemId = $Item->Id();
654  }
655 
656  # if no priority was provided, use the default
657  if ($TaskPriority === NULL)
658  {
659  $TaskPriority = self::$TaskPriority;
660  }
661 
662  # assemble task description
663  $Title = $Item->GetMapped("Title");
664  if (!strlen($Title))
665  {
666  $Title = "Item #".$ItemId;
667  }
668  $TaskDescription = "Update search data for"
669  ." <a href=\"r".$ItemId."\"><i>"
670  .$Title."</i></a>";
671 
672  # queue update
673  $GLOBALS["AF"]->QueueUniqueTask(array(__CLASS__, "RunUpdateForItem"),
674  array(intval($ItemId)), $TaskPriority, $TaskDescription);
675  }
676 
681  public static function RunUpdateForItem($ItemId)
682  {
683  # bail out if item no longer exists
684  try
685  {
686  $Resource = new Resource($ItemId);
687  }
688  catch (InvalidArgumentException $Exception)
689  {
690  return;
691  }
692 
693  # bail out if item is a temporary record
694  if ($Resource->IsTempResource()) { return; }
695 
696  # retrieve schema ID of item to use for item type
697  $ItemType = $Resource->SchemaId();
698 
699  # update search data for resource
700  $SearchEngine = new SPTSearchEngine();
701  $SearchEngine->UpdateForItem($ItemId, $ItemType);
702  }
703 
712  public static function GetResultFacets($SearchResults, $User)
713  {
714  # classifications and names associated with these search results
715  $SearchClasses = array();
716  $SearchNames = array();
717 
718  # make sure we're not faceting too many resources
719  $SearchResults = array_slice(
720  $SearchResults, 0,
721  self::$NumResourcesForFacets,
722  TRUE);
723 
724  # disable DB cache for the search suggestions process,
725  # this avoids memory exhaustion.
726  $DB = new Database();
727  $DB->Caching(FALSE);
728 
729  # number of resources to include in a chunk
730  # a mysql BIGINT is at most 21 characters long and the
731  # default max_packet_size is 1 MiB, so we can pack about
732  # 1 MiB / (22 bytes) = 47,663 ResourceIds into a query before
733  # we need to worry about length problems
734  $ChunkSize = 47600;
735 
736  if (count($SearchResults)>0)
737  {
738  foreach (array_chunk($SearchResults, $ChunkSize, TRUE) as $Chunk)
739  {
740  # pull out all the Classifications that were associated
741  # with our search results along with all their parents
742  $DB->Query("SELECT ResourceId,ClassificationId FROM ResourceClassInts "
743  ."WHERE ResourceId IN "
744  ."(".implode(",", array_keys($Chunk)).")");
745  $Rows = $DB->FetchRows();
746  foreach ($Rows as $Row)
747  {
748  $CurId = $Row["ClassificationId"];
749  while ($CurId !== FALSE)
750  {
751  $SearchClasses[$CurId][]=$Row["ResourceId"] ;
752  $CurId = self::FindParentClass($CurId);
753  }
754  }
755 
756  # also pull out controlled names
757  $DB->Query("SELECT ResourceId,ControlledNameId FROM ResourceNameInts "
758  ."WHERE ResourceId in "
759  ."(".implode(",", array_keys($Chunk)).")");
760  $Rows = $DB->FetchRows();
761  foreach ($Rows as $Row)
762  {
763  $SearchNames[$Row["ControlledNameId"]][]= $Row["ResourceId"];
764  }
765  }
766 
767  # make sure we haven't double-counted resources that have
768  # a classification and some of its children assigned
769  $TmpClasses = array();
770  foreach ($SearchClasses as $ClassId => $Resources)
771  {
772  $TmpClasses[$ClassId] = array_unique($Resources);
773  }
774  $SearchClasses = $TmpClasses;
775  }
776 
777  # generate a map of FieldId -> Field Names for all of the generated facets:
778  $SuggestionsById = array();
779 
780  # pull relevant Classification names out of the DB
781  if (count($SearchClasses)>0)
782  {
783  foreach (array_chunk($SearchClasses, $ChunkSize, TRUE) as $Chunk)
784  {
785  $DB->Query("SELECT FieldId,ClassificationId,ClassificationName"
786  ." FROM Classifications"
787  ." WHERE ClassificationId"
788  ." IN (".implode(",", array_keys($Chunk)).")");
789  foreach ($DB->FetchRows() as $Row)
790  {
791  $SuggestionsById[$Row["FieldId"]][]=
792  array("Id" => $Row["ClassificationId"],
793  "Name" => $Row["ClassificationName"],
794  "Count" => count(
795  $SearchClasses[$Row["ClassificationId"]]));
796  }
797  }
798  }
799 
800  # pull relevant ControlledNames out of the DB
801  if (count($SearchNames)>0)
802  {
803  foreach (array_chunk($SearchNames, $ChunkSize, TRUE) as $Chunk)
804  {
805  $DB->Query("SELECT FieldId,ControlledNameId,ControlledName"
806  ." FROM ControlledNames"
807  ." WHERE ControlledNameId"
808  ." IN (".implode(",", array_keys($SearchNames)).")");
809  foreach ($DB->FetchRows() as $Row)
810  {
811  $SuggestionsById[$Row["FieldId"]][]=
812  array("Id" => $Row["ControlledNameId"],
813  "Name" => $Row["ControlledName"],
814  "Count" => count(
815  $SearchNames[$Row["ControlledNameId"]]));
816  }
817  }
818  }
819 
820  # translate the suggestions that we have in terms of the
821  # FieldIds to suggestions in terms of the field names
822  $SuggestionsByFieldName = array();
823 
824  # if we have suggestions to offer
825  if (count($SuggestionsById)>0)
826  {
827  # gill in an array that maps FieldNames to search links
828  # which would be appropriate for that field
829  foreach ($SuggestionsById as $FieldId => $FieldValues)
830  {
831  try
832  {
833  $ThisField = new MetadataField($FieldId);
834  }
835  catch (Exception $Exception)
836  {
837  $ThisField = NULL;
838  }
839 
840  # bail on fields that didn't exist and on fields that the
841  # current user cannot view, and on fields that are
842  # disabled for advanced searching
843  if (is_object($ThisField) &&
844  $ThisField->Status() == MetadataSchema::MDFSTAT_OK &&
845  $ThisField->IncludeInFacetedSearch() &&
846  $ThisField->Enabled() &&
847  $User->HasPriv($ThisField->ViewingPrivileges()))
848  {
849  $SuggestionsByFieldName[$ThisField->Name()] = array();
850 
851  foreach ($FieldValues as $Value)
852  {
853  $SuggestionsByFieldName[$ThisField->Name()][$Value["Id"]] =
854  array("Name" => $Value["Name"], "Count" => $Value["Count"] );
855  }
856  }
857  }
858  }
859 
860  ksort($SuggestionsByFieldName);
861 
862  return $SuggestionsByFieldName;
863  }
864 
870  public static function SetUpdatePriority($NewPriority)
871  {
872  self::$TaskPriority = $NewPriority;
873  }
874 
879  public static function SetNumResourcesForFacets($NumToUse)
880  {
881  self::$NumResourcesForFacets = $NumToUse;
882  }
883 
884  # ---- BACKWARD COMPATIBILITY --------------------------------------------
885 
905  public function GroupedSearch(
906  $SearchGroups, $StartingResult = 0, $NumberOfResults = 10,
907  $SortByField = NULL, $SortDescending = TRUE)
908  {
909  if ($SearchGroups instanceof SearchParameterSet)
910  {
911  # if search parameter set was passed in, use it directly
912  $SearchParams = $SearchGroups;
913  }
914  else
915  {
916  # otherwise, convert legacy array into SearchParameterSet
917  $SearchParams = new SearchParameterSet();
918  $SearchParams->SetFromLegacyArray($SearchGroups);
919  }
920 
921  # perform search
922  $Results = $this->Search(
923  $SearchParams, $StartingResult, $NumberOfResults,
924  $SortByField, $SortDescending);
925 
926  # pull out the resoults for the Resource schema
927  if (isset($Results[MetadataSchema::SCHEMAID_DEFAULT]))
928  {
929  $Results = $Results[MetadataSchema::SCHEMAID_DEFAULT];
930  }
931  else
932  {
933  $Results = array();
934  }
935 
936  # return the results
937  return $Results;
938  }
939 
959  public static function ConvertToDisplayParameters($SearchParams)
960  {
961  # create display parameters, used to make a more user-friendly
962  # version of the search
963  $DisplayParams = new SearchParameterSet();
964 
965  # copy keyword searches as is
966  $DisplayParams->AddParameter(
967  $SearchParams->GetKeywordSearchStrings() );
968 
969  # copy field searches as is
970  $SearchStrings = $SearchParams->GetSearchStrings();
971  foreach ($SearchStrings as $FieldId => $Params)
972  {
973  $DisplayParams->AddParameter($Params, $FieldId);
974  }
975 
976  # iterate over the search groups, looking for the 'is or begins
977  # with' group that we add when faceting and displaying them as
978  # IS parameters rather than the literal subgroup that we
979  # actually use
980  $Groups = $SearchParams->GetSubgroups();
981  foreach ($Groups as $Group)
982  {
983  # start off assuming that we'll just copy the group without conversion
984  $CopyGroup = TRUE;
985 
986  # if this group uses OR logic for a single field, then it
987  # might be one of the subgroups we want to match and will require further
988  # investigation
989  if ($Group->Logic() == "OR" &&
990  count($Group->GetFields()) == 1)
991  {
992  # pull out the search strings for this field
993  $SearchStrings = $Group->GetSearchStrings();
994  $FieldId = key($SearchStrings);
995  $Values = current($SearchStrings);
996 
997  # check if there are two search strings, one an 'is'
998  # and the other a 'begins with' that both start with the
999  # same prefix, as would be added by the search facet code
1000  if ( count($Values) == 2 &&
1001  preg_match('/^=(.*)$/', $Values[0], $FirstMatch) &&
1002  preg_match('/^\\^(.*) -- $/', $Values[1], $SecondMatch) &&
1003  $FirstMatch[1] == $SecondMatch[1] )
1004  {
1005  # check if this field is valid anywhere
1007  {
1008  $Field = new MetadataField($FieldId);
1009 
1010  # and check if this field is a tree field
1011  if ($Field->Type() == MetadataSchema::MDFTYPE_TREE)
1012  {
1013  # okay, this group matches the form that
1014  # the faceting code would add for an 'is or
1015  # begins with' group; convert it to just an
1016  # 'is' group for display
1017  $DisplayParams->AddParameter("=".$FirstMatch[1], $FieldId);
1018  $CopyGroup = FALSE;
1019  }
1020  }
1021  }
1022  }
1023 
1024  # if this group didn't require conversion, attempt to copy
1025  # it verbatim
1026  if ($CopyGroup)
1027  {
1028  try
1029  {
1030  $DisplayParams->AddSet($Group);
1031  }
1032  catch (Exception $e)
1033  {
1034  # if group could not be added for any reason, skip
1035  # it and move on to the next group
1036  }
1037  }
1038  }
1039 
1040  return $DisplayParams;
1041  }
1042 
1043  # ---- PRIVATE INTERFACE -------------------------------------------------
1044 
1045  private $FieldTypes;
1046  private $Schemas;
1047 
1048  private static $TaskPriority = ApplicationFramework::PRIORITY_BACKGROUND;
1049  private static $NumResourcesForFacets = 500;
1050 
1059  private function GetTextComparisonSql($DBField, $Operator, $Value, $Prefix="")
1060  {
1061  # if we were given a prefix, add the necessary period so we can use it
1062  if (strlen($Prefix))
1063  {
1064  $Prefix = $Prefix.".";
1065  }
1066 
1067  if ($Operator == "^")
1068  {
1069  $EscapedValue = str_replace(
1070  array("%", "_"),
1071  array("\%", "\_"),
1072  addslashes($Value));
1073  $Comparison = $Prefix."`".$DBField."` LIKE '".$EscapedValue."%' ";
1074  }
1075  elseif ($Operator == '$')
1076  {
1077  $EscapedValue = str_replace(
1078  array("%", "_"),
1079  array("\%", "\_"),
1080  addslashes($Value));
1081  $Comparison = $Prefix."`".$DBField."` LIKE '%".$EscapedValue."' ";
1082  }
1083  elseif ($Operator == '!=')
1084  {
1085  $Comparison =
1086  "(".$Prefix."`".$DBField."` ".$Operator." '".addslashes($Value)."'"
1087  ." AND ".$Prefix."`".$DBField."` IS NOT NULL)";
1088  }
1089  else
1090  {
1091  $Comparison = $Prefix."`".$DBField."` "
1092  .$Operator." '".addslashes($Value)."' ";
1093  }
1094  return $Comparison;
1095  }
1096 
1105  private function GetTimeComparisonSql($Field, $Operator, $Value)
1106  {
1107  # check if this is a field modification comparison
1108  $ModificationComparison = ($Operator{0} == "@") ? TRUE : FALSE;
1109 
1110  # if value appears to have time component or text description
1111  if (strpos($Value, ":")
1112  || strstr($Value, "day")
1113  || strstr($Value, "week")
1114  || strstr($Value, "month")
1115  || strstr($Value, "year")
1116  || strstr($Value, "hour")
1117  || strstr($Value, "minute"))
1118  {
1119  # adjust operator if necessary
1120  if ($Operator == "@")
1121  {
1122  $Operator = ">=";
1123  }
1124  else
1125  {
1126  if ($ModificationComparison)
1127  {
1128  $Operator = substr($Operator, 1);
1129  }
1130 
1131  if (strstr($Value, "ago"))
1132  {
1133  $OperatorFlipMap = array(
1134  "<" => ">=",
1135  ">" => "<=",
1136  "<=" => ">",
1137  ">=" => "<",
1138  );
1139  $Operator = isset($OperatorFlipMap[$Operator])
1140  ? $OperatorFlipMap[$Operator] : $Operator;
1141  }
1142  }
1143 
1144  # translate common words-to-numbers
1145  $WordsForNumbers = array(
1146  '/^a /i' => '1 ',
1147  '/^an /i' => '1 ',
1148  '/^one /i' => '1 ',
1149  '/^two /i' => '2 ',
1150  '/^three /i' => '3 ',
1151  '/^four /i' => '4 ',
1152  '/^five /i' => '5 ',
1153  '/^six /i' => '6 ',
1154  '/^seven /i' => '7 ',
1155  '/^eight /i' => '8 ',
1156  '/^nine /i' => '9 ',
1157  '/^ten /i' => '10 ',
1158  '/^eleven /i' => '11 ',
1159  '/^twelve /i' => '12 ',
1160  '/^thirteen /i' => '13 ',
1161  '/^fourteen /i' => '14 ',
1162  '/^fifteen /i' => '15 ',
1163  '/^sixteen /i' => '16 ',
1164  '/^seventeen /i' => '17 ',
1165  '/^eighteen /i' => '18 ',
1166  '/^nineteen /i' => '19 ',
1167  '/^twenty /i' => '20 ',
1168  '/^thirty /i' => '30 ',
1169  '/^forty /i' => '40 ',
1170  '/^fourty /i' => '40 ', # (common misspelling)
1171  '/^fifty /i' => '50 ',
1172  '/^sixty /i' => '60 ',
1173  '/^seventy /i' => '70 ',
1174  '/^eighty /i' => '80 ',
1175  '/^ninety /i' => '90 ');
1176  $Value = preg_replace(
1177  array_keys($WordsForNumbers), $WordsForNumbers, $Value);
1178 
1179  # use strtotime method to build condition
1180  $TimestampValue = strtotime($Value);
1181  if (($TimestampValue !== FALSE) && ($TimestampValue != -1))
1182  {
1183  if ((date("H:i:s", $TimestampValue) == "00:00:00")
1184  && (strpos($Value, "00:00") === FALSE)
1185  && ($Operator == "<="))
1186  {
1187  $NormalizedValue =
1188  date("Y-m-d", $TimestampValue)." 23:59:59";
1189  }
1190  else
1191  {
1192  $NormalizedValue = date(
1193  "Y-m-d H:i:s", $TimestampValue);
1194  }
1195  }
1196  else
1197  {
1198  $NormalizedValue = addslashes($Value);
1199  }
1200 
1201  # build SQL conditional
1202  if ($ModificationComparison)
1203  {
1204  $Conditional = " ( FieldId = ".$Field->Id()
1205  ." AND Timestamp ".$Operator
1206  ." '".$NormalizedValue."' ) ";
1207  }
1208  else
1209  {
1210  $Conditional = " ( `".$Field->DBFieldName()."` "
1211  .$Operator." '".$NormalizedValue."' ) ";
1212  }
1213  }
1214  else
1215  {
1216  # adjust operator if necessary
1217  if ($ModificationComparison)
1218  {
1219  $Operator = ($Operator == "@") ? ">="
1220  : substr($Operator, 1);
1221  }
1222 
1223  # use Date object method to build conditional
1224  $Date = new Date($Value);
1225  if ($Date->Precision())
1226  {
1227  if ($ModificationComparison)
1228  {
1229  $Conditional = " ( FieldId = ".$Field->Id()
1230  ." AND ".$Date->SqlCondition(
1231  "Timestamp", NULL, $Operator)." ) ";
1232  }
1233  else
1234  {
1235  $Conditional = " ( ".$Date->SqlCondition(
1236  $Field->DBFieldName(), NULL, $Operator)." ) ";
1237  }
1238  }
1239  }
1240 
1241  # return assembled conditional to caller
1242  return $Conditional;
1243  }
1244 
1253  private function GetUserComparisonSql(
1254  $FieldId, $Operator, $UserId)
1255  {
1256  switch ($Operator)
1257  {
1258  case "=":
1259  return "(UserId = ".intval($UserId)." AND FieldId = "
1260  .intval($FieldId).")";
1261  break;
1262 
1263  case "!=":
1264  return "(UserId != ".intval($UserId)." AND FieldId = "
1265  .intval($FieldId).")";
1266  break;
1267 
1268  default:
1269  throw new Exception(
1270  "Operator ".$Operator." is not supported for User fields");
1271  break;
1272  }
1273  }
1274 
1282  private static function FindParentClass($ClassId)
1283  {
1284  static $ParentMap;
1285 
1286  # first time through, fetch the mapping of parent values we need
1287  if (!isset($ParentMap))
1288  {
1289  $DB = new Database();
1290 
1291  # result here will be a parent/child mapping for all used
1292  # classifications; avoid caching it as it can be quite large
1293  $PreviousSetting = $DB->Caching();
1294  $DB->Caching(FALSE);
1295  $DB->Query(
1296  "SELECT ParentId, ClassificationId FROM Classifications "
1297  ."WHERE DEPTH > 0 AND FullResourceCount > 0 "
1298  ."AND FieldId IN (SELECT FieldId FROM MetadataFields "
1299  ." WHERE IncludeInFacetedSearch=1)"
1300  );
1301  $DB->Caching($PreviousSetting);
1302 
1303  $ParentMap = $DB->FetchColumn("ParentId", "ClassificationId");
1304  }
1305 
1306  return isset($ParentMap[$ClassId]) ? $ParentMap[$ClassId] : FALSE;
1307  }
1308 }
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.
static QueueUpdateForItem($ItemOrItemId, $TaskPriority=NULL)
Queue background update for an item.