storePath = $storePath; $this->queryBuilderProperties = $queryBuilderProperties; $this->primaryKey = $primaryKey; } /** * @param bool $getOneDocument * @param bool $reduceAndJoinPossible * @return array * @throws IOException * @throws InvalidArgumentException */ public function findDocuments(bool $getOneDocument, bool $reduceAndJoinPossible): array { $queryBuilderProperties = $this->queryBuilderProperties; $dataPath = $this->getDataPath(); $primaryKey = $this->primaryKey; $found = []; // Start collecting and filtering data. IoHelper::checkRead($dataPath); $conditions = $queryBuilderProperties["whereConditions"]; $distinctFields = $queryBuilderProperties["distinctFields"]; $nestedWhereConditions = $queryBuilderProperties["nestedWhere"]; $listOfJoins = $queryBuilderProperties["listOfJoins"]; $search = $queryBuilderProperties["search"]; $searchOptions = $queryBuilderProperties["searchOptions"]; $groupBy = $queryBuilderProperties["groupBy"]; $havingConditions = $queryBuilderProperties["havingConditions"]; $fieldsToSelect = $queryBuilderProperties["fieldsToSelect"]; $orderBy = $queryBuilderProperties["orderBy"]; $skip = $queryBuilderProperties["skip"]; $limit = $queryBuilderProperties["limit"]; $fieldsToExclude = $queryBuilderProperties["fieldsToExclude"]; unset($queryBuilderProperties); if ($handle = opendir($dataPath)) { while (false !== ($entry = readdir($handle))) { if ($entry === "." || $entry === "..") { continue; } $documentPath = $dataPath . $entry; try{ $data = IoHelper::getFileContent($documentPath); } catch (Exception $exception){ continue; } $data = @json_decode($data, true); if (!is_array($data)) { continue; } $storePassed = true; // Append only passed data from this store. // Process conditions if(!empty($conditions)) { // Iterate each conditions. $storePassed = ConditionsHandler::handleWhereConditions($conditions, $data); } // TODO remove nested where with version 3.0 $storePassed = ConditionsHandler::handleNestedWhere($data, $storePassed, $nestedWhereConditions); if ($storePassed === true && count($distinctFields) > 0) { $storePassed = ConditionsHandler::handleDistinct($found, $data, $distinctFields); } if ($storePassed === true) { $found[] = $data; // if we just check for existence or want to return the first item, we dont need to look for more documents if ($getOneDocument === true) { break; } } } closedir($handle); } // apply additional changes to result like sort and limit if($reduceAndJoinPossible === true){ DocumentReducer::joinData($found, $listOfJoins); } if (count($found) > 0) { self::performSearch($found, $search, $searchOptions); } if ($reduceAndJoinPossible === true && !empty($groupBy) && count($found) > 0) { DocumentReducer::handleGroupBy( $found, $groupBy, $fieldsToSelect ); } if($reduceAndJoinPossible === true && empty($groupBy) && count($found) > 0){ // select specific fields DocumentReducer::selectFields($found, $primaryKey, $fieldsToSelect); } if(count($found) > 0){ self::handleHaving($found, $havingConditions); } if($reduceAndJoinPossible === true && count($found) > 0){ // exclude specific fields DocumentReducer::excludeFields($found, $fieldsToExclude); } if(count($found) > 0){ // sort the data. self::sort($found, $orderBy); } if(count($found) > 0) { // Skip data self::skip($found, $skip); } if(count($found) > 0) { // Limit data. self::limit($found, $limit); } return $found; } /** * @return string */ private function getDataPath(): string { return $this->storePath . Store::dataDirectory; } /** * @param array $found * @param array $orderBy * @throws InvalidArgumentException */ private static function sort(array &$found, array $orderBy){ if (!empty($orderBy)) { $resultSortArray = []; foreach ($orderBy as $orderByClause){ // Start sorting on all data. $order = $orderByClause['order']; $fieldName = $orderByClause['fieldName']; $arrayColumn = []; // Get value of the target field. foreach ($found as $value) { $arrayColumn[] = NestedHelper::getNestedValue($fieldName, $value); } $resultSortArray[] = $arrayColumn; // Decide the order direction. // order will be asc or desc (check is done in QueryBuilder class) $resultSortArray[] = ($order === 'asc') ? SORT_ASC : SORT_DESC; } if(!empty($resultSortArray)){ $resultSortArray[] = &$found; array_multisort(...$resultSortArray); } unset($resultSortArray); } } /** * @param array $found * @param $skip */ private static function skip(array &$found, $skip){ if (empty($skip) || $skip <= 0) { return; } $found = array_slice($found, $skip); } /** * @param array $found * @param $limit */ private static function limit(array &$found, $limit){ if (empty($limit) || $limit <= 0) { return; } $found = array_slice($found, 0, $limit); } /** * Do a search in store objects. This is like a doing a full-text search. * @param array $found * @param array $search * @param array $searchOptions * @throws InvalidArgumentException */ private static function performSearch(array &$found, array $search, array $searchOptions) { if(empty($search)){ return; } $minLength = $searchOptions["minLength"]; $searchScoreKey = $searchOptions["scoreKey"]; $searchMode = $searchOptions["mode"]; $searchAlgorithm = $searchOptions["algorithm"]; $scoreMultiplier = 64; $encoding = "UTF-8"; $fields = $search["fields"]; $query = $search["query"]; $lowerQuery = mb_strtolower($query, $encoding); $exactQuery = preg_quote($query, "/"); $fieldsLength = count($fields); $highestScore = $scoreMultiplier ** $fieldsLength; // split query $searchWords = preg_replace('/(\s)/u', ',', $query); $searchWords = explode(",", $searchWords); $prioritizeAlgorithm = (in_array($searchAlgorithm, [ Query::SEARCH_ALGORITHM["prioritize"], Query::SEARCH_ALGORITHM["prioritize_position"] ], true)); $positionAlgorithm = ($searchAlgorithm === Query::SEARCH_ALGORITHM["prioritize_position"]); // apply min word length $temp = []; foreach ($searchWords as $searchWord){ if(strlen($searchWord) >= $minLength){ $temp[] = $searchWord; } } $searchWords = $temp; unset($temp); $searchWords = array_map(static function($value){ return preg_quote($value, "/"); }, $searchWords); // apply mode if($searchMode === "and"){ $preg = ""; foreach ($searchWords as $searchWord){ $preg .= "(?=.*".$searchWord.")"; } $preg = '/^' . $preg . '.*/im'; $pregOr = '!(' . implode('|', $searchWords) . ')!i'; } else { $preg = '!(' . implode('|', $searchWords) . ')!i'; } // search foreach ($found as $foundKey => &$document) { $searchHits = 0; $searchScore = 0; foreach ($fields as $key => $field) { if($prioritizeAlgorithm){ $score = $highestScore / ($scoreMultiplier ** $key); } else { $score = $scoreMultiplier; } $value = NestedHelper::getNestedValue($field, $document); if (!is_string($value) || $value === "") { continue; } $lowerValue = mb_strtolower($value, $encoding); if ($lowerQuery === $lowerValue) { // exact match $searchHits++; $searchScore += 16 * $score; } elseif ($positionAlgorithm && mb_strpos($lowerValue, $lowerQuery, 0, $encoding) === 0) { // exact beginning match $searchHits++; $searchScore += 8 * $score; } elseif ($matches = preg_match_all('!' . $exactQuery . '!i', $value)) { // exact query match $searchHits += $matches; // $searchScore += 2 * $score; $searchScore += $matches * 2 * $score; if($searchAlgorithm === Query::SEARCH_ALGORITHM["hits_prioritize"]){ $searchScore += $matches * ($fieldsLength - $key); } } $matchesArray = []; $matches = ($searchMode === "and") ? preg_match($preg, $value) : preg_match_all($preg, $value, $matchesArray, PREG_OFFSET_CAPTURE); if ($matches) { // any match $searchHits += $matches; $searchScore += $matches * $score; if($searchAlgorithm === Query::SEARCH_ALGORITHM["hits_prioritize"]) { $searchScore += $matches * ($fieldsLength - $key); } // because the "and" search algorithm at most finds one match we also use the amount of word occurrences if($searchMode === "and" && isset($pregOr) && ($matches = preg_match_all($pregOr, $value, $matchesArray, PREG_OFFSET_CAPTURE))){ $searchHits += $matches; $searchScore += $matches * $score; } } // we apply a small very small number to the score to differentiate the distance from the beginning if($positionAlgorithm && $matches && !empty($matchesArray)){ $hitPosition = $matchesArray[0][0][1]; if(!is_int($hitPosition) || !($hitPosition > 0)){ $hitPosition = 1; } $searchScore += ($score / $highestScore) * ($hitPosition / ($hitPosition * $hitPosition)); } } if($searchHits > 0){ if(!is_null($searchScoreKey)){ $document[$searchScoreKey] = $searchScore; } } else { unset($found[$foundKey]); } } } /** * @param array $found * @param array $havingConditions * @throws InvalidArgumentException */ private static function handleHaving(array &$found, array $havingConditions){ if(empty($havingConditions)){ return; } foreach ($found as $key => $document){ if(false === ConditionsHandler::handleWhereConditions($havingConditions, $document)){ unset($found[$key]); } } } }