CWIS Developer Documentation

- Home
- Main Page
- Namespaces
- Classes
- Files

- File List
- File Members

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
10 class SPTSearchEngine extends SearchEngine
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 {
35 case MetadataSchema::MDFTYPE_TEXT:
36 case MetadataSchema::MDFTYPE_PARAGRAPH:
37 case MetadataSchema::MDFTYPE_USER:
38 case MetadataSchema::MDFTYPE_TREE:
39 case MetadataSchema::MDFTYPE_CONTROLLEDNAME:
40 case MetadataSchema::MDFTYPE_OPTION:
41 case MetadataSchema::MDFTYPE_IMAGE:
42 case MetadataSchema::MDFTYPE_FILE:
43 case MetadataSchema::MDFTYPE_URL:
44 case MetadataSchema::MDFTYPE_REFERENCE:
45 $FieldType = self::FIELDTYPE_TEXT;
46 break;
47
48 case MetadataSchema::MDFTYPE_NUMBER:
49 case MetadataSchema::MDFTYPE_FLAG:
50 $FieldType = self::FIELDTYPE_NUMERIC;
51 break;
52
53 case MetadataSchema::MDFTYPE_DATE:
54 $FieldType = self::FIELDTYPE_DATERANGE;
55 break;
56
57 case MetadataSchema::MDFTYPE_TIMESTAMP:
58 $FieldType = self::FIELDTYPE_DATE;
59 break;
60
61 case MetadataSchema::MDFTYPE_POINT:
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 {
134 case MetadataSchema::MDFTYPE_TEXT:
135 case MetadataSchema::MDFTYPE_PARAGRAPH:
136 case MetadataSchema::MDFTYPE_FILE:
137 case MetadataSchema::MDFTYPE_URL:
138 $QueryString = "SELECT DISTINCT ResourceId FROM Resources "
139 ."WHERE POSITION('".$SearchPhrase."'"
140 ." IN LOWER(`".$Field->DBFieldName()."`)) ";
141 break;
142
143 case MetadataSchema::MDFTYPE_IMAGE:
144 $QueryString = "SELECT DISTINCT ResourceId FROM Resources "
145 ."WHERE POSITION('".$SearchPhrase."'"
146 ." IN LOWER(`".$Field->DBFieldName()."AltText`)) ";
147 break;
148
149 case MetadataSchema::MDFTYPE_CONTROLLEDNAME:
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
168 case MetadataSchema::MDFTYPE_OPTION:
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
177 case MetadataSchema::MDFTYPE_TREE:
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
187 case MetadataSchema::MDFTYPE_USER:
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
201 case MetadataSchema::MDFTYPE_NUMBER:
202 if ($SearchPhrase > 0)
203 {
204 $QueryString = "SELECT DISTINCT ResourceId FROM Resources "
205 ."WHERE `".$Field->DBFieldName()
206 ."` = ".(int)$SearchPhrase;
207 }
208 break;
209
210 case MetadataSchema::MDFTYPE_FLAG:
211 case MetadataSchema::MDFTYPE_DATE:
212 case MetadataSchema::MDFTYPE_TIMESTAMP:
213 case MetadataSchema::MDFTYPE_REFERENCE:
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
269 public function SearchFieldsForComparisonMatches(
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
279 if (!MetadataSchema::FieldExistsInAnySchema($FieldId) )
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 {
292 case MetadataSchema::MDFTYPE_TEXT:
293 case MetadataSchema::MDFTYPE_PARAGRAPH:
294 case MetadataSchema::MDFTYPE_NUMBER:
295 case MetadataSchema::MDFTYPE_FLAG:
296 case MetadataSchema::MDFTYPE_URL:
297 $QueryConditions["Resources"][] = $this->GetTextComparisonSql(
298 $Field->DBFieldName(), $Operator, $Value);
299 break;
300
301 case MetadataSchema::MDFTYPE_USER:
302 $User = new CWUser($Value);
303 $QueryConditions["ResourceUserInts"][] =
304 $this->GetUserComparisonSql(
305 $FieldId, $Operator, $User->Id());
306 break;
307
308 case MetadataSchema::MDFTYPE_CONTROLLEDNAME:
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
361 case MetadataSchema::MDFTYPE_OPTION:
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
388 case MetadataSchema::MDFTYPE_TREE:
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
427 case MetadataSchema::MDFTYPE_TIMESTAMP:
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
438 case MetadataSchema::MDFTYPE_DATE:
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
449 case MetadataSchema::MDFTYPE_REFERENCE:
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 }
MetadataSchema\MDFTYPE_IMAGE
const MDFTYPE_IMAGE
Definition: MetadataSchema.php:31
MetadataSchema\MDFTYPE_TIMESTAMP
const MDFTYPE_TIMESTAMP
Definition: MetadataSchema.php:25
SearchEngine\AddField
AddField($FieldId, $FieldType, $ItemTypes, $Weight, $UsedInKeywordSearch)
Add field to include in searching.
Definition: SearchEngine.php:66
SPTSearchEngine\FilterTextDisplay
static FilterTextDisplay($ParamDesc)
Filter for text display of search parameters.
Definition: SPTSearchEngine.php:1005
SearchEngine\$DB
$DB
Definition: SearchEngine.php:714
SPTSearchEngine
Definition: SPTSearchEngine.php:10
MetadataSchema\MDFTYPE_FILE
const MDFTYPE_FILE
Definition: MetadataSchema.php:32
MetadataSchema
Metadata schema (in effect a Factory class for MetadataField).
Definition: MetadataSchema.php:13
SearchParameterSet
Set of parameters used to perform a search.
Definition: SearchParameterSet.php:13
SPTSearchEngine\SetUpdatePriority
static SetUpdatePriority($NewPriority)
Set the default priority for background tasks.
Definition: SPTSearchEngine.php:886
Database
SQL database abstraction object with smart query caching.
Definition: Database.php:22
SPTSearchEngine\SetNumResourcesForFacets
static SetNumResourcesForFacets($NumToUse)
Set the number of resources used for search facets.
Definition: SPTSearchEngine.php:895
MetadataSchema\MDFTYPE_REFERENCE
const MDFTYPE_REFERENCE
Definition: MetadataSchema.php:35
MetadataSchema\MDFSTAT_OK
const MDFSTAT_OK
Definition: MetadataSchema.php:43
MetadataSchema\MDFTYPE_NUMBER
const MDFTYPE_NUMBER
Definition: MetadataSchema.php:23
SPTSearchEngine\QueueDBRebuildForSchema
static QueueDBRebuildForSchema($SchemaId)
Queue background rebuild of search database for all items for specified schema.
Definition: SPTSearchEngine.php:906
MetadataSchema\MDFTYPE_TREE
const MDFTYPE_TREE
Definition: MetadataSchema.php:27
Date
Definition: Date.php:18
MetadataSchema\MDFTYPE_OPTION
const MDFTYPE_OPTION
Definition: MetadataSchema.php:29
MetadataSchema\MDFTYPE_FLAG
const MDFTYPE_FLAG
Definition: MetadataSchema.php:26
MetadataSchema\MDFTYPE_CONTROLLEDNAME
const MDFTYPE_CONTROLLEDNAME
Definition: MetadataSchema.php:28
SPTSearchEngine\GetItemIdsSortedByField
static GetItemIdsSortedByField($ItemType, $FieldId, $SortDescending)
Return item IDs sorted by a specified field.
Definition: SPTSearchEngine.php:646
MetadataSchema\GetAllSchemaIds
static GetAllSchemaIds()
Get IDs for all existing metadata schemas.
Definition: MetadataSchema.php:1823
PHP
PHP
Definition: RSSClient--Test.php:12
MetadataSchema\SCHEMAID_DEFAULT
const SCHEMAID_DEFAULT
Definition: MetadataSchema.php:53
SPTSearchEngine\RunUpdateForItem
static RunUpdateForItem($ItemId)
Update search index for an item.
Definition: SPTSearchEngine.php:697
MetadataSchema\FieldExistsInAnySchema
static FieldExistsInAnySchema($Field)
Determine if a Field exists in any schema.
Definition: MetadataSchema.php:1538
SPTSearchEngine\GroupedSearch
GroupedSearch($SearchGroups, $StartingResult=0, $NumberOfResults=10, $SortByField=NULL, $SortDescending=TRUE)
Perform search with logical groups of fielded searches.
Definition: SPTSearchEngine.php:960
SPTSearchEngine\SearchFieldForPhrases
SearchFieldForPhrases($FieldId, $Phrase)
Perform phrase searching.
Definition: SPTSearchEngine.php:125
MetadataField
Object representing a locally-defined type of metadata field.
Definition: MetadataField.php:13
MetadataSchema\MDFTYPE_PARAGRAPH
const MDFTYPE_PARAGRAPH
Definition: MetadataSchema.php:22
MetadataSchema\MDFTYPE_DATE
const MDFTYPE_DATE
Definition: MetadataSchema.php:24
SearchEngine\DMsg
DMsg($Level, $Msg)
Print debug message if level set high enough.
Definition: SearchEngine.php:2237
Resource
Represents a "resource" in CWIS.
Definition: Resource.php:13
MetadataSchema\MDFTYPE_URL
const MDFTYPE_URL
Definition: MetadataSchema.php:33
SPTSearchEngine\GetResultFacets
static GetResultFacets($SearchResults, $User)
Generate a list of suggested additional search terms that can be used for faceted searching...
Definition: SPTSearchEngine.php:728
MetadataSchema\MDFTYPE_TEXT
const MDFTYPE_TEXT
Definition: MetadataSchema.php:21
SPTSearchEngine\GetFieldContent
GetFieldContent($ItemId, $FieldId)
Overloaded version of method to retrieve text from DB.
Definition: SPTSearchEngine.php:89
SearchEngine
Core metadata archive search engine class.
Definition: SearchEngine.php:13
MetadataSchema\MDFTYPE_USER
const MDFTYPE_USER
Definition: MetadataSchema.php:30
SearchEngine\Search
Search($SearchParams)
Perform search with specified parameters, returning results in a flat array indexed by item ID...
Definition: SearchEngine.php:130
SPTSearchEngine\SearchFieldsForComparisonMatches
SearchFieldsForComparisonMatches($FieldIds, $Operators, $Values, $Logic)
Perform comparison searches.
Definition: SPTSearchEngine.php:269
SearchEngine\DebugLevel
DebugLevel($NewValue)
Set debug output level.
Definition: SearchEngine.php:112
MetadataSchema\MDFTYPE_POINT
const MDFTYPE_POINT
Definition: MetadataSchema.php:34
ResourceFactory
Factory for Resource objects.
Definition: ResourceFactory.php:13
CWUser
CWIS-specific user class.
Definition: CWUser.php:13
MetadataSchema\GetAllSchemas
static GetAllSchemas()
Get all existing metadata schemas.
Definition: MetadataSchema.php:1844
SPTSearchEngine\__construct
__construct()
Class constructor.
Definition: SPTSearchEngine.php:15
ApplicationFramework\PRIORITY_BACKGROUND
const PRIORITY_BACKGROUND
Lowest priority.
Definition: ApplicationFramework.php:2905
SPTSearchEngine\QueueUpdateForItem
static QueueUpdateForItem($ItemOrItemId, $TaskPriority=NULL)
Queue background update for an item.
Definition: SPTSearchEngine.php:659
SPTSearchEngine\QueueDBRebuildForAllSchemas
static QueueDBRebuildForAllSchemas()
Queue background rebuild of search database for all items for all schemas.
Definition: SPTSearchEngine.php:926