3 # FILE: SearchEngine.php 5 # Open Source Metadata Archive Search Engine (OSMASE) 6 # Copyright 2002-2016 Edward Almasy and Internet Scout Research Group 7 # http://scout.wisc.edu 16 # ---- PUBLIC INTERFACE -------------------------------------------------- 18 # possible types of logical operators 22 # flags used for indicating field types 28 # flags used for indicating word states 44 # create database object for our use 47 # save item access parameters 52 # set default debug state 66 public function AddField($FieldId, $FieldType, $ItemTypes,
67 $Weight, $UsedInKeywordSearch)
70 $this->FieldInfo[$FieldId][
"FieldType"] = $FieldType;
71 $this->FieldInfo[$FieldId][
"Weight"] = $Weight;
72 $this->FieldInfo[$FieldId][
"InKeywordSearch"] =
73 $UsedInKeywordSearch ? TRUE : FALSE;
74 $this->FieldInfo[$FieldId][
"ItemTypes"] = is_array($ItemTypes)
75 ? $ItemTypes : array($ItemTypes);
85 return $this->FieldInfo[$FieldId][
"FieldType"];
95 return $this->FieldInfo[$FieldId][
"Weight"];
105 return $this->FieldInfo[$FieldId][
"InKeywordSearch"];
118 # ---- search functions 132 return self::FlattenMultiTypeResults($this->
SearchAll($SearchParams));
146 # if keyword search string was passed in 147 if (is_string($SearchParams))
149 # convert string to search parameter set 150 $SearchString = $SearchParams;
152 $SearchParams->AddParameter($SearchString);
155 # interpret and filter out magic debugging keyword (if any) 156 $KeywordStrings = $SearchParams->GetKeywordSearchStrings();
157 foreach ($KeywordStrings as $String)
159 $FilteredString = $this->ExtractDebugLevel($String);
160 if ($FilteredString != $String)
162 $SearchParams->RemoveParameter($String);
163 $SearchParams->AddParameter($FilteredString);
166 $this->
DMsg(0,
"Description: ".$SearchParams->TextDescription());
168 # save start time to use in calculating search time 169 $StartTime = microtime(TRUE);
171 # clear parsed search term list 172 $this->SearchTermList = array();
175 $Scores = $this->RawSearch($SearchParams);
177 # count, sort, and trim search result scores list 178 $Scores = $this->SortScores($Scores, $SearchParams->SortBy(),
179 $SearchParams->SortDescending());
182 $this->LastSearchTime = microtime(TRUE) - $StartTime;
184 # return search results to caller 185 $this->
DMsg(0,
"Ended up with ".$this->NumberOfResultsAvailable.
" results");
195 # save filter function name 196 $this->FilterFuncs[] = $FunctionName;
207 return ($ItemType === NULL) ? $this->NumberOfResultsAvailable
208 : (isset($this->NumberOfResultsPerItemType[$ItemType])
209 ? $this->NumberOfResultsPerItemType[$ItemType] : 0);
218 return $this->SearchTermList;
239 $FieldIds = $SearchParams->GetFields();
240 foreach ($FieldIds as $FieldId)
242 if (array_key_exists($FieldId, $this->FieldInfo))
244 $Weight += $this->FieldInfo[$FieldId][
"Weight"];
247 if (count($SearchParams->GetKeywordSearchStrings()))
249 foreach ($this->FieldInfo as $FieldId => $Info)
251 if ($Info[
"InKeywordSearch"])
253 $Weight += $Info[
"Weight"];
261 # ---- search database update functions 270 # clear word count added flags for this item 271 unset($this->WordCountAdded);
273 # delete any existing info for this item 274 $this->DB->Query(
"DELETE FROM SearchWordCounts WHERE ItemId = ".$ItemId);
275 $this->DB->Query(
"DELETE FROM SearchItemTypes WHERE ItemId = ".$ItemId);
278 $this->DB->Query(
"INSERT INTO SearchItemTypes (ItemId, ItemType)" 279 .
" VALUES (".intval($ItemId).
", ".intval($ItemType).
")");
281 # for each metadata field 282 foreach ($this->FieldInfo as $FieldId => $Info)
284 # if valid search weight for field and field applies to this item 285 if (($Info[
"Weight"] > 0)
286 && in_array($ItemType, $Info[
"ItemTypes"]))
288 # retrieve text for field 294 # for each text string in array 295 foreach ($Text as $String)
297 # record search info for text 298 $this->RecordSearchInfoForText($ItemId, $FieldId,
299 $Info[
"Weight"], $String,
300 $Info[
"InKeywordSearch"]);
305 # record search info for text 306 $this->RecordSearchInfoForText($ItemId, $FieldId,
307 $Info[
"Weight"], $Text,
308 $Info[
"InKeywordSearch"]);
322 # retrieve IDs for specified number of items starting at specified ID 323 $this->DB->Query(
"SELECT ".$this->ItemIdFieldName.
", ".$this->ItemTypeFieldName
324 .
" FROM ".$this->ItemTableName
325 .
" WHERE ".$this->ItemIdFieldName.
" >= ".$StartingItemId
326 .
" ORDER BY ".$this->ItemIdFieldName.
" LIMIT ".$NumberOfItems);
327 $ItemIds = $this->DB->FetchColumn(
328 $this->ItemTypeFieldName, $this->ItemIdFieldName);
330 # for each retrieved item ID 331 foreach ($ItemIds as $ItemId => $ItemType)
333 # update search info for item 337 # return ID of last item updated to caller 347 # drop all entries pertaining to item from word count table 348 $this->DB->Query(
"DELETE FROM SearchWordCounts WHERE ItemId = ".$ItemId);
349 $this->DB->Query(
"DELETE FROM SearchItemTypes WHERE ItemId = ".$ItemId);
358 # drop all entries pertaining to field from word counts table 359 $this->DB->Query(
"DELETE FROM SearchWordCounts WHERE FieldId = \'".$FieldId.
"\'");
368 return $this->DB->Query(
"SELECT COUNT(*) AS TermCount" 369 .
" FROM SearchWords",
"TermCount");
378 return $this->DB->Query(
"SELECT COUNT(DISTINCT ItemId) AS ItemCount" 379 .
" FROM SearchWordCounts",
"ItemCount");
391 # asssume no synonyms will be added 395 $WordId = $this->GetWordId($Word, TRUE);
397 # for each synonym passed in 398 foreach ($Synonyms as $Synonym)
401 $SynonymId = $this->GetWordId($Synonym, TRUE);
403 # if synonym is not already in database 404 $this->DB->Query(
"SELECT * FROM SearchWordSynonyms" 405 .
" WHERE (WordIdA = ".$WordId
406 .
" AND WordIdB = ".$SynonymId.
")" 407 .
" OR (WordIdB = ".$WordId
408 .
" AND WordIdA = ".$SynonymId.
")");
409 if ($this->DB->NumRowsSelected() == 0)
411 # add synonym entry to database 412 $this->DB->Query(
"INSERT INTO SearchWordSynonyms" 413 .
" (WordIdA, WordIdB)" 414 .
" VALUES (".$WordId.
", ".$SynonymId.
")");
419 # report to caller number of new synonyms added 432 $WordId = $this->GetWordId($Word);
435 if ($WordId !== NULL)
437 # if no specific synonyms provided 438 if ($Synonyms === NULL)
440 # remove all synonyms for word 441 $this->DB->Query(
"DELETE FROM SearchWordSynonyms" 442 .
" WHERE WordIdA = '".$WordId.
"'" 443 .
" OR WordIdB = '".$WordId.
"'");
447 # for each specified synonym 448 foreach ($Synonyms as $Synonym)
450 # look up ID for synonym 451 $SynonymId = $this->GetWordId($Synonym);
453 # if synonym ID was found 454 if ($SynonymId !== NULL)
456 # delete synonym entry 457 $this->DB->Query(
"DELETE FROM SearchWordSynonyms" 458 .
" WHERE (WordIdA = '".$WordId.
"'" 459 .
" AND WordIdB = '".$SynonymId.
"')" 460 .
" OR (WordIdB = '".$WordId.
"'" 461 .
" AND WordIdA = '".$SynonymId.
"')");
473 $this->DB->Query(
"DELETE FROM SearchWordSynonyms");
483 # assume no synonyms will be found 486 # look up ID for word 487 $WordId = $this->GetWordId($Word);
489 # if word ID was found 490 if ($WordId !== NULL)
492 # look up IDs of all synonyms for this word 493 $this->DB->Query(
"SELECT WordIdA, WordIdB FROM SearchWordSynonyms" 494 .
" WHERE WordIdA = ".$WordId
495 .
" OR WordIdB = ".$WordId);
496 $SynonymIds = array();
497 while ($Record = $this->DB->FetchRow)
499 $SynonymIds[] = ($Record[
"WordIdA"] == $WordId)
500 ? $Record[
"WordIdB"] : $Record[
"WordIdA"];
503 # for each synonym ID 504 foreach ($SynonymIds as $SynonymId)
506 # look up synonym word and add to synonym list 507 $Synonyms[] = $this->GetWord($SynonymId);
511 # return synonyms to caller 521 # assume no synonyms will be found 522 $SynonymList = array();
524 # for each synonym ID pair 526 $OurDB->Query(
"SELECT WordIdA, WordIdB FROM SearchWordSynonyms");
527 while ($Record = $OurDB->FetchRow())
530 $Word = $this->GetWord($Record[
"WordIdA"]);
531 $Synonym = $this->GetWord($Record[
"WordIdB"]);
533 # if we do not already have an entry for the word 534 # or synonym is not listed for this word 535 if (!isset($SynonymList[$Word])
536 || !in_array($Synonym, $SynonymList[$Word]))
538 # add entry for synonym 539 $SynonymList[$Word][] = $Synonym;
542 # if we do not already have an entry for the synonym 543 # or word is not listed for this synonym 544 if (!isset($SynonymList[$Synonym])
545 || !in_array($Word, $SynonymList[$Synonym]))
548 $SynonymList[$Synonym][] = $Word;
553 # (this loop removes reciprocal duplicates) 554 foreach ($SynonymList as $Word => $Synonyms)
556 # for each synonym for that word 557 foreach ($Synonyms as $Synonym)
559 # if synonym has synonyms and word is one of them 560 if (isset($SynonymList[$Synonym])
561 && isset($SynonymList[$Word])
562 && in_array($Word, $SynonymList[$Synonym])
563 && in_array($Synonym, $SynonymList[$Word]))
565 # if word has less synonyms than synonym 566 if (count($SynonymList[$Word])
567 < count($SynonymList[$Synonym]))
569 # remove synonym from synonym list for word 570 $SynonymList[$Word] = array_diff(
571 $SynonymList[$Word], array($Synonym));
573 # if no synonyms left for word 574 if (!count($SynonymList[$Word]))
576 # remove empty synonym list for word 577 unset($SynonymList[$Word]);
582 # remove word from synonym list for synonym 583 $SynonymList[$Synonym] = array_diff(
584 $SynonymList[$Synonym], array($Word));
586 # if no synonyms left for word 587 if (!count($SynonymList[$Synonym]))
589 # remove empty synonym list for word 590 unset($SynonymList[$Synonym]);
597 # sort array alphabetically (just for convenience) 598 foreach ($SynonymList as $Word => $Synonyms)
600 asort($SynonymList[$Word]);
604 # return 2D array of synonyms to caller 615 # remove all existing synonyms 618 # for each synonym entry passed in 619 foreach ($SynonymList as $Word => $Synonyms)
621 # add synonyms for word 636 # asssume no synonyms will be added 639 # read in contents of file 640 $Lines = file($FileName, FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES);
642 # if file contained lines 645 # for each line of file 646 foreach ($Lines as $Line)
648 # if line is not a comment 649 if (!preg_match(
"/[\s]*#/", $Line))
651 # split line into words 652 $Words = preg_split(
"/[\s,]+/", $Line);
655 if (count($Words) > 1)
657 # separate out word and synonyms 658 $Word = array_shift($Words);
667 # return count of synonyms added to caller 681 foreach ($Results as $ItemType => $ItemScores)
683 $FlatScores += $ItemScores;
699 $DB->Query(
"SELECT * FROM SearchItemTypes");
700 $ItemTypes =
$DB->FetchColumn(
"ItemType",
"ItemId");
703 foreach ($Results as $ItemId => $ItemScore)
705 $ItemType = $ItemTypes[$ItemId];
706 $SplitScores[$ItemType][$ItemId] = $ItemScore;
712 # ---- PRIVATE INTERFACE ------------------------------------------------- 726 private $ExcludedTermCount;
729 private $InclusiveTermCount;
730 private $RequiredTermCount;
731 private $RequiredTermCounts;
732 private $SearchTermList;
733 private $WordCountAdded;
739 # ---- private methods (searching) 750 # retrieve search strings 754 # add keyword searches (if any) to fielded searches 755 if (count($KeywordSearchStrings))
757 $SearchStrings[self::KEYWORD_FIELD_ID] = $KeywordSearchStrings;
760 # normalize search strings 761 $NormalizedSearchStrings = array();
762 foreach ($SearchStrings as $FieldId => $SearchStringArray)
764 if (!is_array($SearchStringArray))
766 $SearchStringArray = array($SearchStringArray);
768 foreach ($SearchStringArray as $String)
770 $String = trim($String);
773 $NormalizedSearchStrings[$FieldId][] = $String;
777 $SearchStrings = $NormalizedSearchStrings;
779 # if we have strings to search for 780 if (count($SearchStrings))
783 $Scores = $this->SearchAcrossFields(
784 $SearchStrings, $SearchParams->
Logic());
790 # perform subgroup search 791 $NewScores = $this->RawSearch($Subgroup);
793 # added subgroup search scores to previous scores as appropriate 796 $Scores = $this->CombineScores(
797 $Scores, $NewScores, $SearchParams->
Logic());
801 $Scores = $NewScores;
804 if (isset($NewScores))
806 $this->
DMsg(2,
"Have ".count($Scores)
807 .
" results after subgroup processing");
810 # pare down results to just allowed item types (if specified) 813 $AllowedItemTypes = $SearchParams->
ItemTypes();
814 foreach ($Scores as $ItemId => $Score)
816 if (!in_array($this->GetItemType($ItemId), $AllowedItemTypes))
818 unset($Scores[$ItemId]);
821 $this->
DMsg(3,
"Have ".count($Scores)
822 .
" results after paring to allowed item types");
825 # return search results to caller 826 return isset($Scores) ? $Scores : array();
836 private function CombineScores($ScoresA, $ScoresB, $Logic)
841 foreach ($ScoresB as $ItemId => $Score)
843 if (isset($Scores[$ItemId]))
845 $Scores[$ItemId] += $Score;
849 $Scores[$ItemId] = $Score;
856 foreach ($ScoresA as $ItemId => $Score)
858 if (isset($ScoresB[$ItemId]))
860 $Scores[$ItemId] = $Score + $ScoresB[$ItemId];
876 private function SearchAcrossFields($SearchStrings, $Logic)
878 # start by assuming no search will be done 882 $this->ExcludedTermCount = 0;
883 $this->InclusiveTermCount = 0;
884 $this->RequiredTermCount = 0;
885 $this->RequiredTermCounts = array();
888 $NeedComparisonSearch = FALSE;
889 foreach ($SearchStrings as $FieldId => $SearchStringArray)
891 # for each search string for this field 892 foreach ($SearchStringArray as $SearchString)
894 # if field is keyword or field is text and does not look 895 # like comparison match 896 $NotComparisonSearch = !preg_match(
897 self::COMPARISON_OPERATOR_PATTERN, $SearchString);
898 if (($FieldId == self::KEYWORD_FIELD_ID)
899 || (isset($this->FieldInfo[$FieldId])
900 && ($this->FieldInfo[$FieldId][
"FieldType"]
901 == self::FIELDTYPE_TEXT)
902 && $NotComparisonSearch))
904 if ($FieldId == self::KEYWORD_FIELD_ID)
906 $this->
DMsg(0,
"Performing keyword search for string \"" 907 .$SearchString.
"\"");
911 $this->
DMsg(0,
"Searching text field " 912 .$FieldId.
" for string \"".$SearchString.
"\"");
915 # normalize text and split into words 917 $this->ParseSearchStringForWords($SearchString, $Logic);
919 # calculate scores for matching items 920 if (count($Words[$FieldId]))
922 $Scores = $this->SearchForWords(
923 $Words[$FieldId], $FieldId, $Scores);
924 $this->
DMsg(3,
"Have " 925 .count($Scores).
" results after word search");
929 $Phrases[$FieldId] = $this->ParseSearchStringForPhrases(
930 $SearchString, $Logic);
933 if (count($Phrases[$FieldId]))
935 $Scores = $this->SearchForPhrases(
936 $Phrases[$FieldId], $Scores, $FieldId, TRUE, FALSE);
937 $this->
DMsg(3,
"Have " 938 .count($Scores).
" results after phrase search");
943 # set flag to indicate possible comparison search candidate found 944 $NeedComparisonSearch = TRUE;
949 # perform comparison searches 950 if ($NeedComparisonSearch)
952 $Scores = $this->SearchForComparisonMatches(
953 $SearchStrings, $Logic, $Scores);
954 $this->
DMsg(3,
"Have ".count($Scores).
" results after comparison search");
957 # if no results found, no required terms, and exclusions specified 958 if ((count($Scores) == 0) &&
959 ($this->RequiredTermCount == 0) &&
960 ($this->ExcludedTermCount > 0) )
962 # determine which item types are implicated for keyword searches 963 $KeywordItemTypes = [];
964 foreach ($this->FieldInfo as $FieldId => $Info)
966 if ($Info[
"InKeywordSearch"])
968 $KeywordItemTypes = array_merge(
973 $KeywordItemTypes = array_unique($KeywordItemTypes);
975 # determine what item types were in use for the fields we 978 foreach ($SearchStrings as $FieldId => $Info)
980 $MyTypes = ($FieldId == self::KEYWORD_FIELD_ID) ?
982 $this->FieldInfo[$FieldId][
"ItemTypes"];
984 $FieldTypes = array_merge(
985 $FieldTypes, $MyTypes);
987 $FieldTypes = array_unique($FieldTypes);
989 # load all records for these field types 990 $Scores = $this->LoadScoresForAllRecords($FieldTypes);
993 # if search results found 996 # for each search text string 997 foreach ($SearchStrings as $FieldId => $SearchStringArray)
999 # for each search string for this field 1000 foreach ($SearchStringArray as $SearchString)
1003 if (($FieldId == self::KEYWORD_FIELD_ID)
1004 || (isset($this->FieldInfo[$FieldId])
1005 && ($this->FieldInfo[$FieldId][
"FieldType"]
1006 == self::FIELDTYPE_TEXT)))
1008 # if there are words in search text 1009 if (isset($Words[$FieldId]))
1011 # handle any excluded words 1012 $Scores = $this->FilterOnExcludedWords(
1013 $Words[$FieldId], $Scores, $FieldId);
1016 # handle any excluded phrases 1017 if (isset($Phrases[$FieldId]))
1019 $Scores = $this->SearchForPhrases(
1020 $Phrases[$FieldId], $Scores,
1021 $FieldId, FALSE, TRUE);
1025 $this->
DMsg(3,
"Have ".count($Scores)
1026 .
" results after processing exclusions");
1029 # strip off any results that don't contain required words 1030 $Scores = $this->FilterOnRequiredWords($Scores);
1033 # return search result scores to caller 1046 private function SearchForWords($Words, $FieldId, $Scores = NULL)
1050 # start with empty search result scores list if none passed in 1051 if ($Scores == NULL)
1057 foreach ($Words as $Word => $Flags)
1060 if ($FieldId == self::KEYWORD_FIELD_ID)
1062 $this->
DMsg(2,
"Performing keyword search for word \"".$Word.
"\"");
1066 $this->
DMsg(2,
"Searching for word \"".$Word.
"\" in field ".$FieldId);
1069 # if word is not excluded 1070 if (!($Flags & self::WORD_EXCLUDED))
1072 # look up record ID for word 1073 $this->
DMsg(2,
"Looking up word \"".$Word.
"\"");
1074 $WordId = $this->GetWordId($Word);
1077 if ($WordId !== NULL)
1079 # look up counts for word 1080 $DB->Query(
"SELECT ItemId,Count FROM SearchWordCounts " 1081 .
"WHERE WordId = ".$WordId
1082 .
" AND FieldId = ".$FieldId);
1083 $Counts =
$DB->FetchColumn(
"Count",
"ItemId");
1085 # if synonym support is enabled 1086 if ($this->SynonymsEnabled)
1088 # look for any synonyms 1089 $DB->Query(
"SELECT WordIdA, WordIdB" 1090 .
" FROM SearchWordSynonyms" 1091 .
" WHERE WordIdA = ".$WordId
1092 .
" OR WordIdB = ".$WordId);
1094 # if synonyms were found 1095 if (
$DB->NumRowsSelected())
1097 # retrieve synonym IDs 1098 $SynonymIds = array();
1099 while ($Record =
$DB->FetchRow())
1101 $SynonymIds[] = ($Record[
"WordIdA"] == $WordId)
1102 ? $Record[
"WordIdB"]
1103 : $Record[
"WordIdA"];
1107 foreach ($SynonymIds as $SynonymId)
1109 # retrieve counts for synonym 1110 $DB->Query(
"SELECT ItemId,Count" 1111 .
" FROM SearchWordCounts" 1112 .
" WHERE WordId = ".$SynonymId
1113 .
" AND FieldId = ".$FieldId);
1114 $SynonymCounts =
$DB->FetchColumn(
"Count",
"ItemId");
1117 foreach ($SynonymCounts as $ItemId => $Count)
1119 # adjust count because it's a synonym 1120 $AdjustedCount = ceil($Count / 2);
1122 # add count to existing counts 1123 if (isset($Counts[$ItemId]))
1125 $Counts[$ItemId] += $AdjustedCount;
1129 $Counts[$ItemId] = $AdjustedCount;
1137 # if stemming is enabled 1138 if ($this->StemmingEnabled)
1140 # retrieve word stem 1141 $Stem = PorterStemmer::Stem($Word);
1143 # if stem was different from word 1147 $this->
DMsg(2,
"Looking up stem \"".$Stem.
"\"");
1148 $StemId = $this->GetStemId($Stem);
1150 # if ID found for stem 1151 if ($StemId !== NULL)
1153 # retrieve counts for stem 1154 $DB->Query(
"SELECT ItemId,Count" 1155 .
" FROM SearchWordCounts" 1156 .
" WHERE WordId = ".$StemId
1157 .
" AND FieldId = ".$FieldId);
1158 $StemCounts =
$DB->FetchColumn(
"Count",
"ItemId");
1161 foreach ($StemCounts as $ItemId => $Count)
1163 # adjust count because it's a stem 1164 $AdjustedCount = ceil($Count / 2);
1166 # add count to existing counts 1167 if (isset($Counts[$ItemId]))
1169 $Counts[$ItemId] += $AdjustedCount;
1173 $Counts[$ItemId] = $AdjustedCount;
1180 # if counts were found 1184 foreach ($Counts as $ItemId => $Count)
1186 # if word flagged as required 1187 if ($Flags & self::WORD_REQUIRED)
1189 # increment required word count for record 1190 if (isset($this->RequiredTermCounts[$ItemId]))
1192 $this->RequiredTermCounts[$ItemId]++;
1196 $this->RequiredTermCounts[$ItemId] = 1;
1200 # add to item record score 1201 if (isset($Scores[$ItemId]))
1203 $Scores[$ItemId] += $Count;
1207 $Scores[$ItemId] = $Count;
1214 # return basic scores to caller 1225 private function ParseSearchStringForPhrases($SearchString, $Logic)
1227 # split into chunks delimited by double quote marks 1228 $Pieces = explode(
"\"", $SearchString); #
" 1230 # for each pair of chunks 1233 while ($Index < count($Pieces)) 1235 # grab phrase from chunk 1236 $Phrase = trim(addslashes($Pieces[$Index - 1])); 1237 $Flags = self::WORD_PRESENT; 1239 # grab first character of phrase 1240 $FirstChar = substr($Pieces[$Index - 2], -1); 1242 # set flags to reflect any option characters 1243 if ($FirstChar == "-
") 1245 $Flags |= self::WORD_EXCLUDED; 1246 if (!isset($Phrases[$Phrase])) 1248 $this->ExcludedTermCount++; 1253 if ((($Logic == "AND
") 1254 && ($FirstChar != "~
")) 1255 || ($FirstChar == "+
")) 1257 $Flags |= self::WORD_REQUIRED; 1258 if (!isset($Phrases[$Phrase])) 1260 $this->RequiredTermCount++; 1263 if (!isset($Phrases[$Phrase])) 1265 $this->InclusiveTermCount++; 1266 $this->SearchTermList[] = $Phrase; 1269 $Phrases[$Phrase] = $Flags; 1271 # move to next pair of chunks 1275 # return phrases to caller 1284 protected function SearchFieldForPhrases($FieldId, $Phrase) 1301 private function SearchForPhrases($Phrases, $Scores, $FieldId,
1302 $ProcessNonExcluded = TRUE, $ProcessExcluded = TRUE)
1304 # if phrases are found 1305 if (count($Phrases) > 0)
1307 # if this is a keyword search 1308 if ($FieldId == self::KEYWORD_FIELD_ID)
1311 foreach ($this->FieldInfo as $KFieldId => $Info)
1313 # if field is marked to be included in keyword searches 1314 if ($Info[
"InKeywordSearch"])
1316 # call ourself with that field 1317 $Scores = $this->SearchForPhrases(
1318 $Phrases, $Scores, $KFieldId,
1319 $ProcessNonExcluded, $ProcessExcluded);
1326 foreach ($Phrases as $Phrase => $Flags)
1328 $this->
DMsg(2,
"Searching for phrase '".$Phrase
1329 .
"' in field ".$FieldId);
1331 # if phrase flagged as excluded and we are doing excluded 1332 # phrases or phrase flagged as non-excluded and we 1333 # are doing non-excluded phrases 1334 if (($ProcessExcluded && ($Flags & self::WORD_EXCLUDED))
1335 || ($ProcessNonExcluded && !($Flags & self::WORD_EXCLUDED)))
1337 # initialize score list if necessary 1338 if ($Scores === NULL) { $Scores = array(); }
1340 # retrieve list of items that contain phrase 1344 # for each item that contains phrase 1345 foreach ($ItemIds as $ItemId)
1347 # if we are doing excluded phrases and phrase 1348 # is flagged as excluded 1349 if ($ProcessExcluded && ($Flags & self::WORD_EXCLUDED))
1351 # knock item off of list 1352 unset($Scores[$ItemId]);
1354 elseif ($ProcessNonExcluded)
1356 # calculate phrase value based on number of 1357 # words and field weight 1358 $PhraseScore = count(preg_split(
"/[\s]+/",
1359 $Phrase, -1, PREG_SPLIT_NO_EMPTY))
1360 * $this->FieldInfo[$FieldId][
"Weight"];
1361 $this->
DMsg(2,
"Phrase score is ".$PhraseScore);
1363 # bump up item record score 1364 if (isset($Scores[$ItemId]))
1366 $Scores[$ItemId] += $PhraseScore;
1370 $Scores[$ItemId] = $PhraseScore;
1373 # if phrase flagged as required 1374 if ($Flags & self::WORD_REQUIRED)
1376 # increment required word count for record 1377 if (isset($this->RequiredTermCounts[$ItemId]))
1379 $this->RequiredTermCounts[$ItemId]++;
1383 $this->RequiredTermCounts[$ItemId] = 1;
1393 # return updated scores to caller 1405 private function FilterOnExcludedWords($Words, $Scores, $FieldId)
1410 foreach ($Words as $Word => $Flags)
1412 # if word flagged as excluded 1413 if ($Flags & self::WORD_EXCLUDED)
1415 # look up record ID for word 1416 $WordId = $this->GetWordId($Word);
1419 if ($WordId !== NULL)
1421 # look up counts for word 1422 $DB->Query(
"SELECT ItemId FROM SearchWordCounts " 1423 .
"WHERE WordId=${WordId} AND FieldId=${FieldId}");
1426 while ($Record =
$DB->FetchRow())
1428 # if item record is in score list 1429 $ItemId = $Record[
"ItemId"];
1430 if (isset($Scores[$ItemId]))
1432 # remove item record from score list 1433 $this->
DMsg(3,
"Filtering out item ".$ItemId
1434 .
" because it contained word \"".$Word.
"\"");
1435 unset($Scores[$ItemId]);
1442 # returned filtered score list to caller 1451 private function FilterOnRequiredWords($Scores)
1453 # if there were required words 1454 if ($this->RequiredTermCount > 0)
1457 foreach ($Scores as $ItemId => $Score)
1459 # if item does not meet required word count 1460 if (!isset($this->RequiredTermCounts[$ItemId])
1461 || ($this->RequiredTermCounts[$ItemId]
1462 < $this->RequiredTermCount))
1465 $this->
DMsg(4,
"Filtering out item ".$ItemId
1466 .
" because it didn't have required word count of " 1467 .$this->RequiredTermCount
1468 .(isset($this->RequiredTermCounts[$ItemId])
1470 .$this->RequiredTermCounts[$ItemId]
1473 unset($Scores[$ItemId]);
1478 # return filtered list to caller 1492 private function SortScores($Scores, $SortByField, $SortDescending)
1494 # perform any requested filtering 1495 $this->
DMsg(0,
"Have ".count($Scores).
" results before filter callbacks");
1498 # save total number of results available 1499 $this->NumberOfResultsAvailable = count($Scores);
1501 # sort search scores into item type bins 1502 $NewScores = array();
1503 foreach ($Scores as $Id => $Score)
1505 $ItemType = $this->GetItemType($Id);
1506 if ($ItemType !== NULL)
1508 $NewScores[$ItemType][$Id] = $Score;
1511 $Scores = $NewScores;
1513 # for each item type 1514 $NewSortByField = array();
1515 $NewSortDescending = array();
1516 foreach ($Scores as $ItemType => $TypeScores)
1518 # normalize sort field parameter 1519 $NewSortByField[$ItemType] = !is_array($SortByField) ? $SortByField
1520 : (isset($SortByField[$ItemType])
1521 ? $SortByField[$ItemType] : FALSE);
1523 # normalize sort direction parameter 1524 $NewSortDescending[$ItemType] = !is_array($SortDescending) ? $SortDescending
1525 : (isset($SortDescending[$ItemType])
1526 ? $SortDescending[$ItemType] : TRUE);
1528 $SortByField = $NewSortByField;
1529 $SortDescending = $NewSortDescending;
1531 # for each item type 1532 foreach ($Scores as $ItemType => $TypeScores)
1534 # save number of results 1535 $this->NumberOfResultsPerItemType[$ItemType] = count($TypeScores);
1537 # if no sorting field specified 1538 if ($SortByField[$ItemType] === FALSE)
1540 # sort result list by score 1541 if ($SortDescending[$ItemType])
1543 arsort($Scores[$ItemType], SORT_NUMERIC);
1547 asort($Scores[$ItemType], SORT_NUMERIC);
1552 # get list of item IDs in sorted order 1553 $SortedIds = $this->GetItemIdsSortedByField($ItemType,
1554 $SortByField[$ItemType], $SortDescending[$ItemType]);
1556 # if we have sorted item IDs 1557 if (count($SortedIds) && count($TypeScores))
1559 # strip sorted ID list down to those that appear in search results 1560 $SortedIds = array_intersect($SortedIds,
1561 array_keys($TypeScores));
1563 # rebuild score list in sorted order 1564 $NewScores = array();
1565 foreach ($SortedIds as $Id)
1567 $NewScores[$Id] = $TypeScores[$Id];
1569 $Scores[$ItemType] = $NewScores;
1573 # sort result list by score 1574 arsort($Scores[$ItemType], SORT_NUMERIC);
1579 # returned cleaned search result scores list to caller 1590 # if filter functions have been set 1591 if (isset($this->FilterFuncs))
1594 foreach ($Scores as $ItemId => $Score)
1596 # for each filter function 1597 foreach ($this->FilterFuncs as $FuncName)
1599 # if filter function return TRUE for item 1600 if (call_user_func($FuncName, $ItemId))
1603 $this->
DMsg(2,
"Filter callback <i>".$FuncName
1604 .
"</i> rejected item ".$ItemId);
1605 unset($Scores[$ItemId]);
1607 # bail out of filter func loop 1614 # return filtered list to caller 1627 private function SearchForComparisonMatches($SearchStrings, $Logic, $Scores)
1631 foreach ($SearchStrings as $SearchFieldId => $SearchStringArray)
1633 # if field is not keyword 1634 if ($SearchFieldId != self::KEYWORD_FIELD_ID)
1636 # for each search string for this field 1637 foreach ($SearchStringArray as $SearchString)
1639 # look for comparison operators 1640 $FoundOperator = preg_match(
1641 self::COMPARISON_OPERATOR_PATTERN,
1642 $SearchString, $Matches);
1644 # if a comparison operator was found 1645 # or this is a field type that is always a comparison search 1646 if ($FoundOperator ||
1647 ($this->FieldInfo[$SearchFieldId][
"FieldType"]
1648 != self::FIELDTYPE_TEXT))
1650 # determine value to compare against 1651 $Value = trim(preg_replace(
1652 self::COMPARISON_OPERATOR_PATTERN,
'\2',
1655 # if no comparison operator was found 1656 if (!$FoundOperator)
1658 # assume comparison is equality 1659 $Operators[$Index] =
"=";
1663 # use operator from comparison match 1664 $Operators[$Index] = $Matches[1];
1667 # if operator was found 1668 if (isset($Operators[$Index]))
1671 $Values[$Index] = $Value;
1674 $FieldIds[$Index] = $SearchFieldId;
1675 $this->
DMsg(3,
"Added comparison (field = <i>" 1676 .$FieldIds[$Index].
"</i> op = <i>" 1677 .$Operators[$Index].
"</i> val = <i>" 1678 .$Values[$Index].
"</i>)");
1680 # move to next comparison array entry 1688 # if comparisons found 1689 if (isset($Operators))
1691 # perform comparisons on fields and gather results 1692 $Results = $this->SearchFieldsForComparisonMatches(
1693 $FieldIds, $Operators, $Values, $Logic);
1695 # if search logic is set to AND 1696 if ($Logic ==
"AND")
1698 # if results were found 1699 if (count($Results))
1701 # if there were no prior results and no terms for keyword search 1702 if ((count($Scores) == 0) && ($this->InclusiveTermCount == 0))
1704 # add all results to scores 1705 foreach ($Results as $ItemId)
1707 $Scores[$ItemId] = 1;
1712 # remove anything from scores that is not part of results 1713 foreach ($Scores as $ItemId => $Score)
1715 if (in_array($ItemId, $Results) == FALSE)
1717 unset($Scores[$ItemId]);
1730 # add result items to scores 1731 if ($Scores === NULL) { $Scores = array(); }
1732 foreach ($Results as $ItemId)
1734 if (isset($Scores[$ItemId]))
1736 $Scores[$ItemId] += 1;
1740 $Scores[$ItemId] = 1;
1746 # return results to caller 1757 private function SetDebugLevel($SearchStrings)
1759 # if search info is an array 1760 if (is_array($SearchStrings))
1762 # for each array element 1763 foreach ($SearchStrings as $FieldId => $SearchStringArray)
1765 # if element is an array 1766 if (is_array($SearchStringArray))
1768 # for each array element 1769 foreach ($SearchStringArray as $Index => $SearchString)
1771 # pull out search string if present 1772 $SearchStrings[$FieldId][$Index] =
1773 $this->ExtractDebugLevel($SearchString);
1778 # pull out search string if present 1779 $SearchStrings[$FieldId] =
1780 $this->ExtractDebugLevel($SearchStringArray);
1786 # pull out search string if present 1787 $SearchStrings = $this->ExtractDebugLevel($SearchStrings);
1790 # return new search info to caller 1791 return $SearchStrings;
1800 private function ExtractDebugLevel($SearchString)
1802 # if search string contains debug level indicator 1803 if (strstr($SearchString,
"DBUGLVL="))
1805 # remove indicator and set debug level 1806 $Level = preg_replace(
"/^\\s*DBUGLVL=([1-9]{1,2}).*/",
"\\1", $SearchString);
1810 $this->
DMsg(0,
"Setting debug level to ".$Level);
1811 $SearchString = preg_replace(
"/\s*DBUGLVL=${Level}\s*/",
"",
1816 # return (possibly) modified search string to caller 1817 return $SearchString;
1825 private function LoadScoresForAllRecords($ItemTypes)
1827 # if no item types were provided return an empty array 1828 if (count($ItemTypes)==0)
1833 # get all the ItemIds belonging to the given types 1834 $this->DB->Query(
"SELECT ".$this->ItemIdFieldName.
" AS ItemId" 1835 .
" FROM ".$this->ItemTableName
1836 .
" WHERE ".$this->ItemTypeFieldName.
" IN(".implode(
",", $ItemTypes).
")");
1838 # return array with all scores to caller 1839 return array_fill_keys($this->DB->FetchColumn(
"ItemId"), 1);
1842 # ---- private methods (search DB building) 1851 private function UpdateWordCount($Word, $ItemId, $FieldId, $Weight = 1)
1853 # retrieve ID for word 1854 $WordIds[] = $this->GetWordId($Word, TRUE);
1856 # if stemming is enabled and word looks appropriate for stemming 1857 if ($this->StemmingEnabled && !is_numeric($Word))
1859 # retrieve stem of word 1860 $Stem = PorterStemmer::Stem($Word, TRUE);
1862 # if stem is different 1865 # retrieve ID for stem of word 1866 $WordIds[] = $this->GetStemId($Stem, TRUE);
1870 # for word and stem of word 1871 foreach ($WordIds as $WordId)
1873 # if word count already added to database 1874 if (isset($this->WordCountAdded[$WordId][$FieldId]))
1877 $this->DB->Query(
"UPDATE SearchWordCounts SET Count=Count+".$Weight
1878 .
" WHERE WordId=".$WordId
1879 .
" AND ItemId=".$ItemId
1880 .
" AND FieldId=".$FieldId);
1884 # add word count to DB 1885 $this->DB->Query(
"INSERT INTO SearchWordCounts" 1886 .
" (WordId, ItemId, FieldId, Count) VALUES" 1887 .
" (".$WordId.
", ".$ItemId.
", ".$FieldId.
", ".$Weight.
")");
1889 # remember that we added count for this word 1890 $this->WordCountAdded[$WordId][$FieldId] = TRUE;
1893 # decrease weight for stem 1894 $Weight = ceil($Weight / 2);
1906 throw new Exception(
"GetFieldContent() not implemented.");
1918 private function RecordSearchInfoForText(
1919 $ItemId, $FieldId, $Weight, $Text, $IncludeInKeyword)
1922 $Words = $this->ParseSearchStringForWords($Text,
"OR", TRUE);
1924 # if there was text left after parsing 1925 if (count($Words) > 0)
1928 foreach ($Words as $Word => $Flags)
1930 # update count for word 1931 $this->UpdateWordCount($Word, $ItemId, $FieldId);
1933 # if text should be included in keyword searches 1934 if ($IncludeInKeyword)
1936 # update keyword field count for word 1937 $this->UpdateWordCount(
1938 $Word, $ItemId, self::KEYWORD_FIELD_ID, $Weight);
1944 # ---- common private methods (used in both searching and DB build) 1956 private function ParseSearchStringForWords(
1957 $SearchString, $Logic, $IgnorePhrases = FALSE)
1959 # strip off any surrounding whitespace 1960 $Text = trim($SearchString);
1962 # define phrase and group search patterns separately, so that we can 1963 # later replace them easily if necessary 1964 $PhraseSearchPattern =
"/\"[^\"]*\"/";
1965 $GroupSearchPattern =
"/\\([^)]*\\)/";
1967 # set up search string normalization replacement strings (NOTE: these 1968 # are performed in sequence, so the order IS SIGNIFICANT) 1969 $ReplacementPatterns = array(
1970 #
get rid of possessive plurals
1971 "/'s[^a-z0-9\\-+~]+/i" =>
" ",
1972 #
get rid of single quotes / apostrophes
1974 #
get rid of phrases
1975 $PhraseSearchPattern =>
" ",
1977 $GroupSearchPattern =>
" ",
1978 # convert everything but alphanumerics and minus/plus/tilde to a space
1979 "/[^a-z0-9\\-+~]+/i" =>
"\\1 ",
1980 # truncate any runs of minus/plus/tilde to just the first
char 1981 "/([~+-])[~+-]+/" =>
"\\1",
1982 # convert two alphanumerics segments separated by a minus into
1983 # both separate words and a single combined word
1984 "/([~+-]?)([a-z0-9]+)-([a-z0-9]+)/i" =>
"\\1\\2 \\1\\3 \\1\\2\\3",
1985 # convert minus/plus/tilde preceded by anything but whitespace to a space
1986 "/([^\\s])[~+-]+/i" =>
"\\1 ",
1987 # convert minus/plus/tilde followed by whitespace to a space
1988 "/[~+-]+\\s/i" =>
" ",
1989 # convert multiple spaces to one space
1993 # if we are supposed to ignore phrasing (series of words in quotes) 1994 # and grouping (series of words surrounded by parens) 1997 # switch phrase removal to double quote removal 1998 # and switch group removal to paren removal 1999 $NewReplacementPatterns = [];
2000 foreach ($ReplacementPatterns as $Pattern => $Replacement)
2002 if ($Pattern == $PhraseSearchPattern)
2006 elseif ($Pattern == $GroupSearchPattern)
2008 $Pattern =
"/[\(\)]+/";
2010 $NewReplacementPatterns[$Pattern] = $Replacement;
2012 $ReplacementPatterns = $NewReplacementPatterns;
2015 # remove punctuation from text and normalize whitespace 2016 $Text = preg_replace(array_keys($ReplacementPatterns),
2017 $ReplacementPatterns, $Text);
2018 $this->
DMsg(2,
"Normalized search string is \"".$Text.
"\"");
2020 # convert text to lower case 2021 $Text = strtolower($Text);
2023 # strip off any extraneous whitespace 2024 $Text = trim($Text);
2026 # start with an empty array 2029 # if we have no words left after parsing 2030 if (strlen($Text) != 0)
2033 foreach (explode(
" ", $Text) as $Word)
2035 # grab first character of word 2036 $FirstChar = substr($Word, 0, 1);
2038 # strip off option characters and set flags appropriately 2039 $Flags = self::WORD_PRESENT;
2040 if ($FirstChar ==
"-")
2042 $Word = substr($Word, 1);
2043 $Flags |= self::WORD_EXCLUDED;
2044 if (!isset($Words[$Word]))
2046 $this->ExcludedTermCount++;
2051 if ($FirstChar ==
"~")
2053 $Word = substr($Word, 1);
2055 elseif (($Logic ==
"AND")
2056 || ($FirstChar ==
"+"))
2058 if ($FirstChar ==
"+")
2060 $Word = substr($Word, 1);
2062 $Flags |= self::WORD_REQUIRED;
2063 if (!isset($Words[$Word]))
2065 $this->RequiredTermCount++;
2068 if (!isset($Words[$Word]))
2070 $this->InclusiveTermCount++;
2071 $this->SearchTermList[] = $Word;
2075 # store flags to indicate word found 2076 $Words[$Word] = $Flags;
2077 $this->
DMsg(3,
"Word identified (".$Word.
")");
2081 # return normalized words to caller 2092 private function GetWordId($Word, $AddIfNotFound = FALSE)
2094 static $WordIdCache;
2096 # if word was in ID cache 2097 if (isset($WordIdCache[$Word]))
2100 $WordId = $WordIdCache[$Word];
2104 # look up ID in database 2105 $WordId = $this->DB->Query(
"SELECT WordId" 2106 .
" FROM SearchWords" 2107 .
" WHERE WordText='".addslashes($Word).
"'",
2110 # if ID was not found and caller requested it be added 2111 if (($WordId === NULL) && $AddIfNotFound)
2113 # add word to database 2114 $this->DB->Query(
"INSERT INTO SearchWords (WordText)" 2115 .
" VALUES ('".addslashes(strtolower($Word)).
"')");
2117 # get ID for newly added word 2118 $WordId = $this->DB->LastInsertId();
2122 $WordIdCache[$Word] = $WordId;
2125 # return ID to caller 2136 private function GetStemId($Stem, $AddIfNotFound = FALSE)
2138 static $StemIdCache;
2140 # if stem was in ID cache 2141 if (isset($StemIdCache[$Stem]))
2144 $StemId = $StemIdCache[$Stem];
2148 # look up ID in database 2149 $StemId = $this->DB->Query(
"SELECT WordId" 2150 .
" FROM SearchStems" 2151 .
" WHERE WordText='".addslashes($Stem).
"'",
2154 # if ID was not found and caller requested it be added 2155 if (($StemId === NULL) && $AddIfNotFound)
2157 # add stem to database 2158 $this->DB->Query(
"INSERT INTO SearchStems (WordText)" 2159 .
" VALUES ('".addslashes(strtolower($Stem)).
"')");
2161 # get ID for newly added stem 2162 $StemId = $this->DB->LastInsertId();
2165 # adjust from DB ID value to stem ID value 2166 $StemId += self::STEM_ID_OFFSET;
2169 $StemIdCache[$Stem] = $StemId;
2172 # return ID to caller 2181 private function GetWord($WordId)
2185 # if word was in cache 2186 if (isset($WordCache[$WordId]))
2188 # use word from cache 2189 $Word = $WordCache[$WordId];
2193 # adjust search location and word ID if word is stem 2194 $TableName =
"SearchWords";
2195 if ($WordId >= self::STEM_ID_OFFSET)
2197 $TableName =
"SearchStems";
2198 $WordId -= self::STEM_ID_OFFSET;
2201 # look up word in database 2202 $Word = $this->DB->Query(
"SELECT WordText" 2203 .
" FROM ".$TableName
2204 .
" WHERE WordId='".$WordId.
"'",
2207 # save word to cache 2208 $WordCache[$WordId] = $Word;
2211 # return word to caller 2220 private function GetItemType($ItemId)
2222 static $ItemTypeCache;
2223 if (!isset($ItemTypeCache))
2225 $this->DB->Query(
"SELECT * FROM SearchItemTypes");
2226 $ItemTypeCache = $this->DB->FetchColumn(
"ItemType",
"ItemId");
2228 return isset($ItemTypeCache[$ItemId])
2229 ? (int)$ItemTypeCache[$ItemId] : NULL;
2237 protected function DMsg($Level, $Msg)
2241 print
"SE: ".$Msg.
"<br>\n";
2245 # ---- BACKWARD COMPATIBILITY -------------------------------------------- 2247 # possible types of logical operators 2251 # pattern to detect search strings that are explicit comparisons
SearchTermCount()
Get total number of search terms indexed by search engine.
SetAllSynonyms($SynonymList)
Set all synonyms.
DropItem($ItemId)
Drop all data pertaining to item from search database.
AddField($FieldId, $FieldType, $ItemTypes, $Weight, $UsedInKeywordSearch)
Add field to include in searching.
RemoveSynonyms($Word, $Synonyms=NULL)
Remove synonym(s).
LoadSynonymsFromFile($FileName)
Load synonyms from a file.
$NumberOfResultsPerItemType
Set of parameters used to perform a search.
SQL database abstraction object with smart query caching.
SearchFieldForPhrases($FieldId, $Phrase)
Search for phrase in specified field.
GetAllSynonyms()
Get all synonyms.
FilterOnSuppliedFunctions($Scores)
Filter search scores through any supplied functions.
Logic($NewValue=NULL)
Get/set logic for set.
UpdateForItem($ItemId, $ItemType)
Update search database for the specified item.
AddSynonyms($Word, $Synonyms)
Add synonyms.
const FIELDTYPE_DATERANGE
SearchAll($SearchParams)
Perform search with specified parameters, returning results separated by item type.
ItemTypes($ItemTypes=NULL)
Get/set allowed item types.
SearchTerms()
Get normalized list of search terms.
NumberOfResults($ItemType=NULL)
Get number of results found by most recent search.
FieldWeight($FieldId)
Get search weight for specified field.
FieldType($FieldId)
Get type of specified field (text/numeric/date/daterange).
ItemCount()
Get total number of items indexed by search engine.
static BuildMultiTypeResults($Results)
Expand a one-dimensional array(ItemId => ItemScore) into a two-dimensional array(ItemType => array(It...
__construct($ItemTableName, $ItemIdFieldName, $ItemTypeFieldName)
Object constructor.
GetSearchStrings($IncludeSubgroups=FALSE)
Get search strings in set.
RemoveAllSynonyms()
Remove all synonyms.
DMsg($Level, $Msg)
Print debug message if level set high enough.
DropField($FieldId)
Drop all data pertaining to field from search database.
GetFieldContent($ItemId, $FieldId)
Retrieve content for specified field for specified item.
Core metadata archive search engine class.
GetKeywordSearchStrings()
Get keyword search strings in set.
$NumberOfResultsAvailable
Search($SearchParams)
Perform search with specified parameters, returning results in a flat array indexed by item ID...
const COMPARISON_OPERATOR_PATTERN
DebugLevel($NewValue)
Set debug output level.
UpdateForItems($StartingItemId, $NumberOfItems)
Update search database for the specified range of items.
FieldedSearchWeightScale($SearchParams)
Get total of weights for all fields involved in search, useful for assessing scale of scores in searc...
static FlattenMultiTypeResults($Results)
Flatten a two-dimensional array keyed by ItemType with results for each type as the outer values into...
FieldInKeywordSearch($FieldId)
Get whether specified field is included in keyword searches.
AddResultFilterFunction($FunctionName)
Add function that will be called to filter search results.
SearchTime()
Get time that last search took, in seconds.
GetSubgroups()
Get parameter subgroups.
GetSynonyms($Word)
Get synonyms for word.