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 new 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 
408  # for tree fields where the user searched "^XYZ --", treat this as
409  # "=XYZ" OR "^XYZ -- "
410  if ($Operator == "^" && preg_match("%^(.+) -- *$%", $Value, $Matches))
411  {
412  $Queries[$QueryIndex] .= "("
413  .$this->GetTextComparisonSql(
414  "ClassificationName", "=", $Matches[1])
415  ." OR "
416  .$this->GetTextComparisonSql(
417  "ClassificationName", "^", $Matches[1]." -- ")
418  .") ";
419  }
420  else
421  {
422  $Queries[$QueryIndex] .= $this->GetTextComparisonSql(
423  "ClassificationName", $Operator, $Value);
424  }
425  break;
426 
428  # if we have an SQL conditional
429  $TimestampConditional = $this->GetTimeComparisonSql(
430  $Field, $Operator, $Value);
431  if ($TimestampConditional)
432  {
433  # add conditional
434  $QueryConditions["Resources"][] = $TimestampConditional;
435  }
436  break;
437 
439  $Date = new Date($Value);
440  if ($Date->Precision())
441  {
442  $QueryConditions["Resources"][] =
443  " ( ".$Date->SqlCondition(
444  $Field->DBFieldName()."Begin",
445  $Field->DBFieldName()."End", $Operator)." ) ";
446  }
447  break;
448 
450  $QueryIndex = "ReferenceInts".$FieldId;
451  if (!isset($Queries[$QueryIndex]))
452  {
453  $Queries[$QueryIndex] =
454  "SELECT DISTINCT RI.SrcResourceId AS ResourceId"
455  ." FROM ReferenceInts AS RI, Resources AS R "
456  ." WHERE RI.FieldId = ".intval($FieldId)
457  ." AND (";
458  $CloseQuery[$QueryIndex] = TRUE;
459  }
460  else
461  {
462  $Queries[$QueryIndex] .= $CombineWord;
463  }
464 
465  if (is_numeric($Value))
466  {
467  # add subquery for specific resource ID
468  $Queries[$QueryIndex] .= "(RI.DstResourceId ".$Operator." '"
469  .addslashes($Value)."')";
470  }
471  else
472  {
473  # iterate over all the schemas this field can reference,
474  # gluing together an array of subqueries for the mapped
475  # title field of each as we go
476  $SchemaIds = $Field->ReferenceableSchemaIds();
477 
478  # if no referenceable schemas configured, fall back to
479  # searching all schemas
480  if (count($SchemaIds)==0)
481  {
482  $SchemaIds = MetadataSchema::GetAllSchemaIds();
483  }
484 
485  $Subqueries = array();
486  foreach ($SchemaIds as $SchemaId)
487  {
488  $Schema = new MetadataSchema($SchemaId);
489  $MappedTitle = $Schema->GetFieldByMappedName("Title");
490 
491  $Subqueries[]= $this->GetTextComparisonSql(
492  $MappedTitle->DBFieldName(), $Operator, $Value, "R");
493  }
494 
495  # OR together all the subqueries, add it to the query
496  # for our field
497  $Queries[$QueryIndex] .=
498  "((".implode(" OR ", $Subqueries).")"
499  ." AND R.ResourceId = RI.DstResourceId)";
500  }
501  break;
502 
503  case "Modification Comparison":
504  # if we have an SQL conditional
505  $TimestampConditional = $this->GetTimeComparisonSql(
506  $Field, $Operator, $Value);
507  if ($TimestampConditional)
508  {
509  # add conditional
510  $QueryConditions["ResourceFieldTimestamps"][] =
511  $TimestampConditional;
512  }
513  break;
514 
515  default:
516  throw new Exception("Search of unknown field type ("
517  .$ProcessingType.").");
518  break;
519  }
520  }
521 
522  # if query conditions found
523  if (isset($QueryConditions))
524  {
525  # for each query condition group
526  foreach ($QueryConditions as $TargetTable => $Conditions)
527  {
528  # add entry with conditions to query list
529  if (isset($Queries[$TargetTable]))
530  {
531  $Queries[$TargetTable] .= $CombineWord
532  .implode($CombineWord, $Conditions);
533  }
534  else
535  {
536  $Queries[$TargetTable] = "SELECT DISTINCT ResourceId"
537  ." FROM ".$TargetTable." WHERE "
538  .implode($CombineWord, $Conditions);
539  }
540  }
541  }
542 
543  # if queries found
544  if (isset($Queries))
545  {
546  # for each assembled query
547  foreach ($Queries as $QueryIndex => $Query)
548  {
549  # if query has multiple parts
550  if (is_array($Query))
551  {
552  # for each part of query
553  $ResourceIds = array();
554  foreach ($Query as $PartIndex => $PartQuery)
555  {
556  # add closing paren if query was flagged to be closed
557  if (isset($CloseQuery[$QueryIndex][$PartIndex]))
558  {
559  $PartQuery .= ") ";
560  if (($Logic == "AND")
561  && ($ComparisonCount[$QueryIndex][$PartIndex] > 1))
562  {
563  $PartQuery .= "GROUP BY ResourceId"
564  ." HAVING COUNT(DISTINCT "
565  .$ComparisonCountField[$QueryIndex][$PartIndex]
566  .") = "
567  .$ComparisonCount[$QueryIndex][$PartIndex];
568  }
569  }
570 
571  # perform query and retrieve IDs
572  $this->DMsg(5, "Performing comparison query <i>"
573  .$PartQuery."</i>");
574  $this->DB->Query($PartQuery);
575  $ResourceIds = $ResourceIds
576  + $this->DB->FetchColumn("ResourceId");
577  $this->DMsg(5, "Comparison query produced <i>"
578  .count($ResourceIds)."</i> results");
579  }
580  }
581  else
582  {
583  # add closing paren if query was flagged to be closed
584  if (isset($CloseQuery[$QueryIndex]))
585  {
586  $Query .= ") ";
587  if (($Logic == "Logic")
588  && ($ComparisonCount[$QueryIndex] > 1))
589  {
590  $Query .= "GROUP BY ResourceId"
591  ." HAVING COUNT(DISTINCT "
592  .$ComparisonCountField[$QueryIndex]
593  .") = "
594  .$ComparisonCount[$QueryIndex];
595  }
596  }
597 
598  # perform query and retrieve IDs
599  $this->DMsg(5, "Performing comparison query <i>".$Query."</i>");
600  $this->DB->Query($Query);
601  $ResourceIds = $this->DB->FetchColumn("ResourceId");
602  $this->DMsg(5, "Comparison query produced <i>"
603  .count($ResourceIds)."</i> results");
604  }
605 
606  # if we already have some results
607  if (isset($Results))
608  {
609  # if search logic is set to AND
610  if ($Logic == "AND")
611  {
612  # remove anything from results that was not returned from query
613  $Results = array_intersect($Results, $ResourceIds);
614  }
615  else
616  {
617  # add values returned from query to results
618  $Results = array_unique(array_merge($Results, $ResourceIds));
619  }
620  }
621  else
622  {
623  # set results to values returned from query
624  $Results = $ResourceIds;
625  }
626  }
627  }
628  else
629  {
630  # initialize results to empty list
631  $Results = array();
632  }
633 
634  # return results to caller
635  return $Results;
636  }
637 
646  public static function GetItemIdsSortedByField(
647  $ItemType, $FieldId, $SortDescending)
648  {
649  $RFactory = new ResourceFactory($ItemType);
650  return $RFactory->GetResourceIdsSortedBy($FieldId, !$SortDescending);
651  }
652 
659  public static function QueueUpdateForItem($ItemOrItemId, $TaskPriority = NULL)
660  {
661  if (is_numeric($ItemOrItemId))
662  {
663  $ItemId = $ItemOrItemId;
664  $Item = new Resource($ItemId);
665  }
666  else
667  {
668  $Item = $ItemOrItemId;
669  $ItemId = $Item->Id();
670  }
671 
672  # if no priority was provided, use the default
673  if ($TaskPriority === NULL)
674  {
675  $TaskPriority = self::$TaskPriority;
676  }
677 
678  # assemble task description
679  $Title = $Item->GetMapped("Title");
680  if (!strlen($Title))
681  {
682  $Title = "Item #".$ItemId;
683  }
684  $TaskDescription = "Update search data for"
685  ." <a href=\"r".$ItemId."\"><i>"
686  .$Title."</i></a>";
687 
688  # queue update
689  $GLOBALS["AF"]->QueueUniqueTask(array(__CLASS__, "RunUpdateForItem"),
690  array(intval($ItemId)), $TaskPriority, $TaskDescription);
691  }
692 
697  public static function RunUpdateForItem($ItemId)
698  {
699  # bail out if item no longer exists
700  try
701  {
702  $Resource = new Resource($ItemId);
703  }
704  catch (InvalidArgumentException $Exception)
705  {
706  return;
707  }
708 
709  # bail out if item is a temporary record
710  if ($Resource->IsTempResource()) { return; }
711 
712  # retrieve schema ID of item to use for item type
713  $ItemType = $Resource->SchemaId();
714 
715  # update search data for resource
716  $SearchEngine = new SPTSearchEngine();
717  $SearchEngine->UpdateForItem($ItemId, $ItemType);
718  }
719 
728  public static function GetResultFacets($SearchResults, $User)
729  {
730  # classifications and names associated with these search results
731  $SearchClasses = array();
732  $SearchNames = array();
733 
734  # make sure we're not faceting too many resources
735  $SearchResults = array_slice(
736  $SearchResults, 0,
737  self::$NumResourcesForFacets,
738  TRUE);
739 
740  # disable DB cache for the search suggestions process,
741  # this avoids memory exhaustion.
742  $DB = new Database();
743  $DB->Caching(FALSE);
744 
745  # number of resources to include in a chunk
746  # a mysql BIGINT is at most 21 characters long and the
747  # default max_packet_size is 1 MiB, so we can pack about
748  # 1 MiB / (22 bytes) = 47,663 ResourceIds into a query before
749  # we need to worry about length problems
750  $ChunkSize = 47600;
751 
752  if (count($SearchResults)>0)
753  {
754  foreach (array_chunk($SearchResults, $ChunkSize, TRUE) as $Chunk)
755  {
756  # pull out all the Classifications that were associated
757  # with our search results along with all their parents
758  $DB->Query("SELECT ResourceId,ClassificationId FROM ResourceClassInts "
759  ."WHERE ResourceId IN "
760  ."(".implode(",", array_keys($Chunk)).")");
761  $Rows = $DB->FetchRows();
762  foreach ($Rows as $Row)
763  {
764  $CurId = $Row["ClassificationId"];
765  while ($CurId !== FALSE)
766  {
767  $SearchClasses[$CurId][]=$Row["ResourceId"] ;
768  $CurId = self::FindParentClass($CurId);
769  }
770  }
771 
772  # also pull out controlled names
773  $DB->Query("SELECT ResourceId,ControlledNameId FROM ResourceNameInts "
774  ."WHERE ResourceId in "
775  ."(".implode(",", array_keys($Chunk)).")");
776  $Rows = $DB->FetchRows();
777  foreach ($Rows as $Row)
778  {
779  $SearchNames[$Row["ControlledNameId"]][]= $Row["ResourceId"];
780  }
781  }
782 
783  # make sure we haven't double-counted resources that have
784  # a classification and some of its children assigned
785  $TmpClasses = array();
786  foreach ($SearchClasses as $ClassId => $Resources)
787  {
788  $TmpClasses[$ClassId] = array_unique($Resources);
789  }
790  $SearchClasses = $TmpClasses;
791  }
792 
793  # generate a map of FieldId -> Field Names for all of the generated facets:
794  $SuggestionsById = array();
795 
796  # pull relevant Classification names out of the DB
797  if (count($SearchClasses)>0)
798  {
799  foreach (array_chunk($SearchClasses, $ChunkSize, TRUE) as $Chunk)
800  {
801  $DB->Query("SELECT FieldId,ClassificationId,ClassificationName"
802  ." FROM Classifications"
803  ." WHERE ClassificationId"
804  ." IN (".implode(",", array_keys($Chunk)).")");
805  foreach ($DB->FetchRows() as $Row)
806  {
807  $SuggestionsById[$Row["FieldId"]][]=
808  array("Id" => $Row["ClassificationId"],
809  "Name" => $Row["ClassificationName"],
810  "Count" => count(
811  $SearchClasses[$Row["ClassificationId"]]));
812  }
813  }
814  }
815 
816  # pull relevant ControlledNames out of the DB
817  if (count($SearchNames)>0)
818  {
819  foreach (array_chunk($SearchNames, $ChunkSize, TRUE) as $Chunk)
820  {
821  $DB->Query("SELECT FieldId,ControlledNameId,ControlledName"
822  ." FROM ControlledNames"
823  ." WHERE ControlledNameId"
824  ." IN (".implode(",", array_keys($SearchNames)).")");
825  foreach ($DB->FetchRows() as $Row)
826  {
827  $SuggestionsById[$Row["FieldId"]][]=
828  array("Id" => $Row["ControlledNameId"],
829  "Name" => $Row["ControlledName"],
830  "Count" => count(
831  $SearchNames[$Row["ControlledNameId"]]));
832  }
833  }
834  }
835 
836  # translate the suggestions that we have in terms of the
837  # FieldIds to suggestions in terms of the field names
838  $SuggestionsByFieldName = array();
839 
840  # if we have suggestions to offer
841  if (count($SuggestionsById)>0)
842  {
843  # gill in an array that maps FieldNames to search links
844  # which would be appropriate for that field
845  foreach ($SuggestionsById as $FieldId => $FieldValues)
846  {
847  try
848  {
849  $ThisField = new MetadataField($FieldId);
850  }
851  catch (Exception $Exception)
852  {
853  $ThisField = NULL;
854  }
855 
856  # bail on fields that didn't exist and on fields that the
857  # current user cannot view, and on fields that are
858  # disabled for advanced searching
859  if (is_object($ThisField) &&
860  $ThisField->Status() == MetadataSchema::MDFSTAT_OK &&
861  $ThisField->IncludeInFacetedSearch() &&
862  $ThisField->Enabled() &&
863  $User->HasPriv($ThisField->ViewingPrivileges()))
864  {
865  $SuggestionsByFieldName[$ThisField->Name()] = array();
866 
867  foreach ($FieldValues as $Value)
868  {
869  $SuggestionsByFieldName[$ThisField->Name()][$Value["Id"]] =
870  array("Name" => $Value["Name"], "Count" => $Value["Count"] );
871  }
872  }
873  }
874  }
875 
876  ksort($SuggestionsByFieldName);
877 
878  return $SuggestionsByFieldName;
879  }
880 
886  public static function SetUpdatePriority($NewPriority)
887  {
888  self::$TaskPriority = $NewPriority;
889  }
890 
895  public static function SetNumResourcesForFacets($NumToUse)
896  {
897  self::$NumResourcesForFacets = $NumToUse;
898  }
899 
906  public static function QueueDBRebuildForSchema($SchemaId)
907  {
908  $SearchEngine = new self();
909 
910  $RFactory = new ResourceFactory($SchemaId);
911  $Ids = $RFactory->GetItemIds();
912 
913  foreach ($Ids as $Id)
914  {
915  $SearchEngine->QueueUpdateForItem($Id);
916  }
917 
918  return count($Ids);
919  }
920 
926  public static function QueueDBRebuildForAllSchemas()
927  {
928  $Schemas = MetadataSchema::GetAllSchemas();
929  $ItemsQueued = 0;
930 
931  foreach ($Schemas as $SchemaId => $Schema)
932  {
933  $ItemsQueued += self::QueueDBRebuildForSchema($SchemaId);
934  }
935 
936  return $ItemsQueued;
937  }
938 
939  # ---- BACKWARD COMPATIBILITY --------------------------------------------
940 
960  public function GroupedSearch(
961  $SearchGroups, $StartingResult = 0, $NumberOfResults = 10,
962  $SortByField = NULL, $SortDescending = TRUE)
963  {
964  # check for use of deprecated parameters
965  if ($StartingResult != 0)
966  {
967  throw new InvalidArgumentException("Deprecated StartingResult"
968  ." parameter used with value \"".$StartingResult."\".");
969  }
970  elseif ($NumberOfResults != 10)
971  {
972  throw new InvalidArgumentException("Deprecated NumberOfResults"
973  ." parameter used with value \"".$NumberOfResults."\".");
974  }
975 
976  if ($SearchGroups instanceof SearchParameterSet)
977  {
978  # if search parameter set was passed in, use it directly
979  $SearchParams = $SearchGroups;
980  }
981  else
982  {
983  # otherwise, convert legacy array into SearchParameterSet
984  $SearchParams = new SearchParameterSet();
985  $SearchParams->SetFromLegacyArray($SearchGroups);
986  }
987 
988  # add sort and item type parameters to search parameter set
989  $SearchParams->SortBy($SortByField);
990  $SearchParams->SortDescending($SortDescending);
991  $SearchParams->ItemTypes(MetadataSchema::SCHEMAID_DEFAULT);
992 
993  # perform search
994  $Results = $this->Search($SearchParams);
995 
996  # return the results
997  return $Results;
998  }
999 
1005  public static function FilterTextDisplay($ParamDesc)
1006  {
1007  $Patterns = [
1008  # translate current version of 'is under'
1009  '%([A-Z].*) begins with <i>(.*) -- ?</i>%' =>
1010  '$1 is under <i>$2</i>',
1011  # translate older subgroup version of 'is under'
1012  '%\( ?([A-Z].*) is <i>(.*)</i><br>\n(&nbsp;)+ or \1 is under <i>\2</i>\)%' =>
1013  '$1 is under <i>$2</i>',
1014  ];
1015 
1016  return preg_replace(
1017  array_keys($Patterns),
1018  array_values($Patterns),
1019  $ParamDesc);
1020  }
1021 
1022  # ---- PRIVATE INTERFACE -------------------------------------------------
1023 
1024  private $FieldTypes;
1025  private $Schemas;
1026 
1027  private static $TaskPriority = ApplicationFramework::PRIORITY_BACKGROUND;
1028  private static $NumResourcesForFacets = 500;
1029 
1038  private function GetTextComparisonSql($DBField, $Operator, $Value, $Prefix="")
1039  {
1040  # if we were given a prefix, add the necessary period so we can use it
1041  if (strlen($Prefix))
1042  {
1043  $Prefix = $Prefix.".";
1044  }
1045 
1046  if ($Operator == "^")
1047  {
1048  $EscapedValue = str_replace(
1049  array("%", "_"),
1050  array("\%", "\_"),
1051  addslashes($Value));
1052  $Comparison = $Prefix."`".$DBField."` LIKE '".$EscapedValue."%' ";
1053  }
1054  elseif ($Operator == '$')
1055  {
1056  $EscapedValue = str_replace(
1057  array("%", "_"),
1058  array("\%", "\_"),
1059  addslashes($Value));
1060  $Comparison = $Prefix."`".$DBField."` LIKE '%".$EscapedValue."' ";
1061  }
1062  elseif ($Operator == '!=')
1063  {
1064  $Comparison =
1065  "(".$Prefix."`".$DBField."` ".$Operator." '".addslashes($Value)."'"
1066  ." AND ".$Prefix."`".$DBField."` IS NOT NULL)";
1067  }
1068  else
1069  {
1070  $Comparison = $Prefix."`".$DBField."` "
1071  .$Operator." '".addslashes($Value)."' ";
1072  }
1073  return $Comparison;
1074  }
1075 
1084  private function GetTimeComparisonSql($Field, $Operator, $Value)
1085  {
1086  # check if this is a field modification comparison
1087  $ModificationComparison = ($Operator{0} == "@") ? TRUE : FALSE;
1088 
1089  # if value appears to have time component or text description
1090  if (strpos($Value, ":")
1091  || strstr($Value, "day")
1092  || strstr($Value, "week")
1093  || strstr($Value, "month")
1094  || strstr($Value, "year")
1095  || strstr($Value, "hour")
1096  || strstr($Value, "minute"))
1097  {
1098  # adjust operator if necessary
1099  if ($Operator == "@")
1100  {
1101  $Operator = ">=";
1102  }
1103  else
1104  {
1105  if ($ModificationComparison)
1106  {
1107  $Operator = substr($Operator, 1);
1108  }
1109 
1110  if (strstr($Value, "ago"))
1111  {
1112  $OperatorFlipMap = array(
1113  "<" => ">=",
1114  ">" => "<=",
1115  "<=" => ">",
1116  ">=" => "<",
1117  );
1118  $Operator = isset($OperatorFlipMap[$Operator])
1119  ? $OperatorFlipMap[$Operator] : $Operator;
1120  }
1121  }
1122 
1123  # translate common words-to-numbers
1124  $WordsForNumbers = array(
1125  '/^a /i' => '1 ',
1126  '/^an /i' => '1 ',
1127  '/^one /i' => '1 ',
1128  '/^two /i' => '2 ',
1129  '/^three /i' => '3 ',
1130  '/^four /i' => '4 ',
1131  '/^five /i' => '5 ',
1132  '/^six /i' => '6 ',
1133  '/^seven /i' => '7 ',
1134  '/^eight /i' => '8 ',
1135  '/^nine /i' => '9 ',
1136  '/^ten /i' => '10 ',
1137  '/^eleven /i' => '11 ',
1138  '/^twelve /i' => '12 ',
1139  '/^thirteen /i' => '13 ',
1140  '/^fourteen /i' => '14 ',
1141  '/^fifteen /i' => '15 ',
1142  '/^sixteen /i' => '16 ',
1143  '/^seventeen /i' => '17 ',
1144  '/^eighteen /i' => '18 ',
1145  '/^nineteen /i' => '19 ',
1146  '/^twenty /i' => '20 ',
1147  '/^thirty /i' => '30 ',
1148  '/^forty /i' => '40 ',
1149  '/^fourty /i' => '40 ', # (common misspelling)
1150  '/^fifty /i' => '50 ',
1151  '/^sixty /i' => '60 ',
1152  '/^seventy /i' => '70 ',
1153  '/^eighty /i' => '80 ',
1154  '/^ninety /i' => '90 ');
1155  $Value = preg_replace(
1156  array_keys($WordsForNumbers), $WordsForNumbers, $Value);
1157 
1158  # use strtotime method to build condition
1159  $TimestampValue = strtotime($Value);
1160  if (($TimestampValue !== FALSE) && ($TimestampValue != -1))
1161  {
1162  if ((date("H:i:s", $TimestampValue) == "00:00:00")
1163  && (strpos($Value, "00:00") === FALSE)
1164  && ($Operator == "<="))
1165  {
1166  $NormalizedValue =
1167  date("Y-m-d", $TimestampValue)." 23:59:59";
1168  }
1169  else
1170  {
1171  $NormalizedValue = date(
1172  "Y-m-d H:i:s", $TimestampValue);
1173  }
1174  }
1175  else
1176  {
1177  $NormalizedValue = addslashes($Value);
1178  }
1179 
1180  # build SQL conditional
1181  if ($ModificationComparison)
1182  {
1183  $Conditional = " ( FieldId = ".$Field->Id()
1184  ." AND Timestamp ".$Operator
1185  ." '".$NormalizedValue."' ) ";
1186  }
1187  else
1188  {
1189  $Conditional = " ( `".$Field->DBFieldName()."` "
1190  .$Operator." '".$NormalizedValue."' ) ";
1191  }
1192  }
1193  else
1194  {
1195  # adjust operator if necessary
1196  if ($ModificationComparison)
1197  {
1198  $Operator = ($Operator == "@") ? ">="
1199  : substr($Operator, 1);
1200  }
1201 
1202  # use Date object method to build conditional
1203  $Date = new Date($Value);
1204  if ($Date->Precision())
1205  {
1206  if ($ModificationComparison)
1207  {
1208  $Conditional = " ( FieldId = ".$Field->Id()
1209  ." AND ".$Date->SqlCondition(
1210  "Timestamp", NULL, $Operator)." ) ";
1211  }
1212  else
1213  {
1214  $Conditional = " ( ".$Date->SqlCondition(
1215  $Field->DBFieldName(), NULL, $Operator)." ) ";
1216  }
1217  }
1218  }
1219 
1220  # return assembled conditional to caller
1221  return $Conditional;
1222  }
1223 
1232  private function GetUserComparisonSql(
1233  $FieldId, $Operator, $UserId)
1234  {
1235  switch ($Operator)
1236  {
1237  case "=":
1238  return "(UserId = ".intval($UserId)." AND FieldId = "
1239  .intval($FieldId).")";
1240  break;
1241 
1242  case "!=":
1243  return "(UserId != ".intval($UserId)." AND FieldId = "
1244  .intval($FieldId).")";
1245  break;
1246 
1247  default:
1248  throw new Exception(
1249  "Operator ".$Operator." is not supported for User fields");
1250  break;
1251  }
1252  }
1253 
1261  private static function FindParentClass($ClassId)
1262  {
1263  static $ParentMap;
1264 
1265  # first time through, fetch the mapping of parent values we need
1266  if (!isset($ParentMap))
1267  {
1268  $DB = new Database();
1269 
1270  # result here will be a parent/child mapping for all used
1271  # classifications; avoid caching it as it can be quite large
1272  $PreviousSetting = $DB->Caching();
1273  $DB->Caching(FALSE);
1274  $DB->Query(
1275  "SELECT ParentId, ClassificationId FROM Classifications "
1276  ."WHERE DEPTH > 0 AND FullResourceCount > 0 "
1277  ."AND FieldId IN (SELECT FieldId FROM MetadataFields "
1278  ." WHERE IncludeInFacetedSearch=1)"
1279  );
1280  $DB->Caching($PreviousSetting);
1281 
1282  $ParentMap = $DB->FetchColumn("ParentId", "ClassificationId");
1283  }
1284 
1285  return isset($ParentMap[$ClassId]) ? $ParentMap[$ClassId] : FALSE;
1286  }
1287 }
AddField($FieldId, $FieldType, $ItemTypes, $Weight, $UsedInKeywordSearch)
Add field to include in searching.
static FilterTextDisplay($ParamDesc)
Filter for text display of search parameters.
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.
static QueueDBRebuildForSchema($SchemaId)
Queue background rebuild of search database for all items for specified schema.
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.
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.
Search($SearchParams)
Perform search with specified parameters, returning results in a flat array indexed by item ID...
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.
static QueueDBRebuildForAllSchemas()
Queue background rebuild of search database for all items for all schemas.