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 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 $Queries[$QueryIndex] .= $this->GetTextComparisonSql(
408 "ClassificationName", $Operator, $Value);
409 break;
410
411 case MetadataSchema::MDFTYPE_TIMESTAMP:
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
422 case MetadataSchema::MDFTYPE_DATE:
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
433 case MetadataSchema::MDFTYPE_REFERENCE:
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
1006 if (MetadataSchema::FieldExistsInAnySchema($FieldId))
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 }
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
SearchEngine\$DB
$DB
Definition: SearchEngine.php:723
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:870
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:879
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
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:630
MetadataSchema\GetAllSchemaIds
static GetAllSchemaIds()
Get IDs for all existing metadata schemas.
Definition: MetadataSchema.php:1807
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:681
MetadataSchema\FieldExistsInAnySchema
static FieldExistsInAnySchema($Field)
Determine if a Field exists in any schema.
Definition: MetadataSchema.php:1522
SPTSearchEngine\GroupedSearch
GroupedSearch($SearchGroups, $StartingResult=0, $NumberOfResults=10, $SortByField=NULL, $SortDescending=TRUE)
Perform search with logical groups of fielded searches.
Definition: SPTSearchEngine.php:905
SPTSearchEngine\SearchFieldForPhrases
SearchFieldForPhrases($FieldId, $Phrase)
Perform phrase searching.
Definition: SPTSearchEngine.php:125
SearchEngine\Search
Search($SearchParams, $StartingResult=0, $NumberOfResults=PHP_INT_MAX, $SortByField=NULL, $SortDescending=TRUE)
Perform search with specified parameters.
Definition: SearchEngine.php:138
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:2246
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:712
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
SPTSearchEngine\ConvertToDisplayParameters
static ConvertToDisplayParameters($SearchParams)
Get a simplified SearchParameterSet for display purposes.
Definition: SPTSearchEngine.php:959
MetadataSchema\MDFTYPE_USER
const MDFTYPE_USER
Definition: MetadataSchema.php:30
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:1828
SPTSearchEngine\__construct
__construct()
Class constructor.
Definition: SPTSearchEngine.php:15
SPTSearchEngine\QueueUpdateForItem
static QueueUpdateForItem($ItemOrItemId, $TaskPriority=NULL)
Queue background update for an item.
Definition: SPTSearchEngine.php:643