/** * Deconstruct a search string and return a list of conversation IDs that fulfill it. * * @param array $channelIDs A list of channel IDs to include results from. * @param string $searchString The search string to deconstruct and find matching conversations. * @param bool $orderBySticky Whether or not to put stickied conversations at the top. * @return array|bool An array of matching conversation IDs, or false if there are none. */ public function getConversationIDs($channelIDs = array(), $searchString = "", $orderBySticky = false) { $this->reset(); $this->trigger("getConversationIDsBefore", array(&$channelIDs, &$searchString, &$orderBySticky)); if ($searchString and $seconds = $this->isFlooding()) { $this->error("search", sprintf(T("message.waitToSearch"), $seconds)); return false; } // Initialize the SQL query that will return the resulting conversation IDs. $this->sql = ET::SQL()->select("c.conversationId")->from("conversation c"); // Only get conversations in the specified channels. if ($channelIDs) { $this->sql->where("c.channelId IN (:channelIds)")->bind(":channelIds", $channelIDs); } // Process the search string into individial terms. Replace all "-" signs with "+!", and then // split the string by "+". Negated terms will then be prefixed with "!". Only keep the first // 5 terms, just to keep the load on the database down! $terms = !empty($searchString) ? explode("+", strtolower(str_replace("-", "+!", trim($searchString, " +-")))) : array(); $terms = array_slice(array_filter($terms), 0, 5); // Take each term, match it with a gambit, and execute the gambit's function. foreach ($terms as $term) { // Are we dealing with a negated search term, ie. prefixed with a "!"? $term = trim($term); if ($negate = $term[0] == "!") { $term = trim($term, "! "); } if ($term[0] == "#") { $term = ltrim($term, "#"); // If the term is an alias, translate it into the appropriate gambit. if (array_key_exists($term, self::$aliases)) { $term = self::$aliases[$term]; } // Find a matching gambit by evaluating each gambit's condition, and run its callback function. foreach (self::$gambits as $gambit) { list($condition, $function) = $gambit; if (eval($condition)) { call_user_func_array($function, array(&$this, $term, $negate)); continue 2; } } } // If we didn't find a gambit, use this term as a fulltext term. if ($negate) { $term = "-" . str_replace(" ", " -", $term); } $this->fulltext($term); } // If an order for the search results has not been specified, apply a default. // Order by sticky and then last post time. if (!count($this->orderBy)) { if ($orderBySticky) { $this->orderBy("c.sticky DESC"); } $this->orderBy("c.lastPostTime DESC"); } // If we're not including ignored conversations, add a where predicate to the query to exclude them. if (!$this->includeIgnored and ET::$session->user) { $q = ET::SQL()->select("conversationId")->from("member_conversation")->where("type='member'")->where("id=:memberIdIgnored")->where("ignored=1")->get(); $this->sql->where("conversationId NOT IN ({$q})")->bind(":memberIdIgnored", ET::$session->userId); } // Now we need to loop through the ID filters and run them one-by-one. When a query returns a selection // of conversation IDs, subsequent queries are restricted to filtering those conversation IDs, // and so on, until we have a list of IDs to pass to the final query. $goodConversationIDs = array(); $badConversationIDs = array(); $idCondition = ""; foreach ($this->idFilters as $v) { list($sql, $negate) = $v; // Apply the list of good IDs to the query. $sql->where($idCondition); // Get the list of conversation IDs so that the next condition can use it in its query. $result = $sql->exec(); $ids = array(); while ($row = $result->nextRow()) { $ids[] = (int) reset($row); } // If this condition is negated, then add the IDs to the list of bad conversations. // If the condition is not negated, set the list of good conversations to the IDs, provided there are some. if ($negate) { $badConversationIDs = array_merge($badConversationIDs, $ids); } elseif (count($ids)) { $goodConversationIDs = $ids; } else { return false; } // Strip bad conversation IDs from the list of good conversation IDs. if (count($goodConversationIDs)) { $goodConversationIds = array_diff($goodConversationIDs, $badConversationIDs); if (!count($goodConversationIDs)) { return false; } } // This will be the condition for the next query that restricts or eliminates conversation IDs. if (count($goodConversationIDs)) { $idCondition = "conversationId IN (" . implode(",", $goodConversationIDs) . ")"; } elseif (count($badConversationIDs)) { $idCondition = "conversationId NOT IN (" . implode(",", $badConversationIDs) . ")"; } } // Reverse the order if necessary - swap DESC and ASC. if ($this->orderReverse) { foreach ($this->orderBy as $k => $v) { $this->orderBy[$k] = strtr($this->orderBy[$k], array("DESC" => "ASC", "ASC" => "DESC")); } } // Now check if there are any fulltext keywords to filter by. if (count($this->fulltext)) { // Run a query against the posts table to get matching conversation IDs. $fulltextString = implode(" ", $this->fulltext); //中文分词,搜索更加准确 $fulltextString = ($s = spiltWords($fulltextString)) ? $s : $fulltextString; $KeywordArray = explode(" ", $fulltextString); //分词后 $this->fulltext = $KeywordArray; $like = ''; $count = count($KeywordArray); foreach ($KeywordArray as $key => $value) { $like .= "(title LIKE '%{$value}%')"; if (ET::$session->user) { $like .= " OR (content LIKE '%{$value}%')"; } if ($key + 1 != $count) { $like .= " OR "; } } if (preg_match('/[\\x80-\\xff]/i', $fulltextString)) { $fulltextQuery = ET::SQL()->select("DISTINCT conversationId")->from("post")->where($like)->where($idCondition)->orderBy("conversationId DESC"); } else { $fulltextQuery = ET::SQL()->select("DISTINCT conversationId")->from("post")->where("MATCH (title, content) AGAINST (:fulltext IN BOOLEAN MODE)")->where($idCondition)->orderBy("MATCH (title, content) AGAINST (:fulltextOrder) DESC")->bind(":fulltext", $fulltextString)->bind(":fulltextOrder", $fulltextString); } $this->trigger("fulltext", array($fulltextQuery, $this->fulltext)); $result = $fulltextQuery->exec(); $ids = array(); while ($row = $result->nextRow()) { $ids[] = reset($row); } // Change the ID condition to this list of matching IDs, and order by relevance. if (count($ids)) { $idCondition = "conversationId IN (" . implode(",", $ids) . ")"; } else { return false; } $this->orderBy = array("FIELD(c.conversationId," . implode(",", $ids) . ")"); } // Set a default limit if none has previously been set. if (!$this->limit) { $this->limit = C("esoTalk.search.limit"); } // Finish constructing the final query using the ID whitelist/blacklist we've come up with. // Get one more result than we'll actually need so we can see if there are "more results." if ($idCondition) { $this->sql->where($idCondition); } $this->sql->orderBy($this->orderBy)->limit($this->limit + 1); // Make sure conversations that the user isn't allowed to see are filtered out. ET::conversationModel()->addAllowedPredicate($this->sql); //load cache $this->ns_key = $this->ns_key ? $this->ns_key : ET::$cache->get(self::CACHE_NS_KEY); $ns_key = $this->ns_key; if ($ns_key === false) { $ns_key = time(); ET::$cache->store(self::CACHE_NS_KEY, $ns_key); } $my_key = self::CACHE_CID_KEY . '_' . $ns_key . '_' . md5($this->sql->__toString()); $conversationIDs = ET::$cache->get($my_key); //当返回数组没有元素时也会当作false判断,所以要判断值和类型 if ($conversationIDs === false) { // Execute the query, and collect the final set of conversation IDs. $result = $this->sql->exec(); $conversationIDs = array(); while ($row = $result->nextRow()) { $conversationIDs[] = reset($row); } ET::$cache->store($my_key, $conversationIDs); } // If there's one more result than we actually need, indicate that there are "more results." if (count($conversationIDs) == $this->limit + 1) { array_pop($conversationIDs); if ($this->limit < C("esoTalk.search.limitMax")) { $this->areMoreResults = true; } } return count($conversationIDs) ? $conversationIDs : false; }
/** * Add a fulltext search WHERE predicate to an SQL query. * * @param ETSQLQuery $sql The SQL query to add the predicate to. * @param string $search The search string. * @return void */ private function whereSearch(&$sql, $search) { //中文分词,搜索更加准确 $fulltextString = ($s = spiltWords($search)) ? $s : $search; $KeywordArray = explode(" ", $fulltextString); $like = ''; $count = count($KeywordArray); foreach ($KeywordArray as $key => $value) { $like .= "(content LIKE '%{$value}%')"; if (ET::$session->user) { $like .= " OR (content LIKE '%{$value}%')"; } if ($key + 1 != $count) { $like .= " OR "; } } if (preg_match('/[\\x80-\\xff]/i', $search)) { $sql->where($like); } else { $sql->where("MATCH (content) AGAINST (:search IN BOOLEAN MODE)")->where("MATCH (title) AGAINST (:search IN BOOLEAN MODE)"); } $sql->where("deleteMemberId IS NULL")->bind(":search", "%" . $search . "%"); $this->trigger("whereSearch", array($sql, $search)); }