Przeglądaj źródła

Light Boolean search implementation (#1828)

* Light Boolean search implementation

"Hello intitle:World OR date:P1D example"
https://github.com/FreshRSS/FreshRSS/issues/879

* Doc Boolean search

* Doc typos
Alexandre Alapetite 8 lat temu
rodzic
commit
84d891f8cf

+ 1 - 1
app/Controllers/entryController.php

@@ -40,7 +40,7 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
 		$get = Minz_Request::param('get');
 		$next_get = Minz_Request::param('nextGet', $get);
 		$id_max = Minz_Request::param('idMax', 0);
-		FreshRSS_Context::$search = new FreshRSS_Search(Minz_Request::param('search', ''));
+		FreshRSS_Context::$search = new FreshRSS_BooleanSearch(Minz_Request::param('search', ''));
 
 		FreshRSS_Context::$state = Minz_Request::param('state', 0);
 		if (FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_FAVORITE)) {

+ 1 - 1
app/Controllers/indexController.php

@@ -182,7 +182,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 			FreshRSS_Context::$state |= FreshRSS_Entry::STATE_READ;
 		}
 
-		FreshRSS_Context::$search = new FreshRSS_Search(Minz_Request::param('search', ''));
+		FreshRSS_Context::$search = new FreshRSS_BooleanSearch(Minz_Request::param('search', ''));
 		FreshRSS_Context::$order = Minz_Request::param(
 			'order', FreshRSS_Context::$user_conf->sort_order
 		);

+ 55 - 0
app/Models/BooleanSearch.php

@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * Contains Boolean search from the search form.
+ */
+class FreshRSS_BooleanSearch {
+
+	private $raw_input = '';
+	private $searches = array();
+
+	public function __construct($input) {
+		$input = trim($input);
+		if ($input == '') {
+			return;
+		}
+		$this->raw_input = $input;
+
+		$input = preg_replace('/:&quot;(.*?)&quot;/', ':"\1"', $input);
+		$splits = preg_split('/\b(OR)\b/i', $input, -1, PREG_SPLIT_DELIM_CAPTURE);
+
+		$segment = '';
+		$ns = count($splits);
+		for ($i = 0; $i < $ns; $i++) {
+			$segment = $segment . $splits[$i];
+			if (trim($segment) == '' || strcasecmp($segment, 'OR') === 0) {
+				$segment = '';
+			} else {
+				$quotes = substr_count($segment, '"') + substr_count($segment, '&quot;');
+				if ($quotes % 2 === 0) {
+					$segment = trim($segment);
+					if ($segment != '') {
+						$this->searches[] = new FreshRSS_Search($segment);
+					}
+					$segment = '';
+				}
+			}
+		}
+		$segment = trim($segment);
+		if ($segment != '') {
+			$this->searches[] = new FreshRSS_Search($segment);
+		}
+	}
+
+	public function searches() {
+		return $this->searches;
+	}
+
+	public function __toString() {
+		return $this->getRawInput();
+	}
+
+	public function getRawInput() {
+		return $this->raw_input;
+	}
+}

+ 101 - 85
app/Models/EntryDAO.php

@@ -437,7 +437,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	 * @param integer $priorityMin
 	 * @return integer affected rows
 	 */
-	public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0, $filter = null, $state = 0) {
+	public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0, $filters = null, $state = 0) {
 		FreshRSS_UserDAO::touch();
 		if ($idMax == 0) {
 			$idMax = time() . '000000';
@@ -454,7 +454,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		}
 		$values = array($idMax);
 
-		list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filter, $state);
+		list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state);
 
 		$stm = $this->bd->prepare($sql . $search);
 		if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
@@ -480,7 +480,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	 * @param integer $idMax fail safe article ID
 	 * @return integer affected rows
 	 */
-	public function markReadCat($id, $idMax = 0, $filter = null, $state = 0) {
+	public function markReadCat($id, $idMax = 0, $filters = null, $state = 0) {
 		FreshRSS_UserDAO::touch();
 		if ($idMax == 0) {
 			$idMax = time() . '000000';
@@ -492,7 +492,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			 . 'WHERE f.category=? AND e.is_read=0 AND e.id <= ?';
 		$values = array($id, $idMax);
 
-		list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filter, $state);
+		list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state);
 
 		$stm = $this->bd->prepare($sql . $search);
 		if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
@@ -518,7 +518,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	 * @param integer $idMax fail safe article ID
 	 * @return integer affected rows
 	 */
-	public function markReadFeed($id_feed, $idMax = 0, $filter = null, $state = 0) {
+	public function markReadFeed($id_feed, $idMax = 0, $filters = null, $state = 0) {
 		FreshRSS_UserDAO::touch();
 		if ($idMax == 0) {
 			$idMax = time() . '000000';
@@ -531,7 +531,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			 . 'WHERE id_feed=? AND is_read=0 AND id <= ?';
 		$values = array($id_feed, $idMax);
 
-		list($searchValues, $search) = $this->sqlListEntriesWhere('', $filter, $state);
+		list($searchValues, $search) = $this->sqlListEntriesWhere('', $filters, $state);
 
 		$stm = $this->bd->prepare($sql . $search);
 		if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
@@ -625,7 +625,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		return 'CONCAT(' . $s1 . ',' . $s2 . ')';	//MySQL
 	}
 
-	protected function sqlListEntriesWhere($alias = '', $filter = null, $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $firstId = '', $date_min = 0) {
+	protected function sqlListEntriesWhere($alias = '', $filters = null, $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $firstId = '', $date_min = 0) {
 		$search = ' ';
 		$values = array();
 		if ($state & FreshRSS_Entry::STATE_NOT_READ) {
@@ -650,10 +650,6 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			default:
 				throw new FreshRSS_EntriesGetter_Exception('Bad order in Entry->listByType: [' . $order . ']!');
 		}
-		/*if ($firstId === '' && parent::$sharedDbType === 'mysql') {
-			//MySQL optimization. TODO: check if this is needed again, after the filtering for old articles has been removed in 0.9-dev
-			$firstId = $order === 'DESC' ? '9000000000'. '000000' : '0';
-		}*/
 		if ($firstId !== '') {
 			$search .= 'AND ' . $alias . 'id ' . ($order === 'DESC' ? '<=' : '>=') . ' ? ';
 			$values[] = $firstId;
@@ -662,91 +658,111 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			$search .= 'AND ' . $alias . 'id >= ? ';
 			$values[] = $date_min . '000000';
 		}
-		if ($filter) {
-			if ($filter->getMinDate()) {
-				$search .= 'AND ' . $alias . 'id >= ? ';
-				$values[] = "{$filter->getMinDate()}000000";
-			}
-			if ($filter->getMaxDate()) {
-				$search .= 'AND ' . $alias . 'id <= ? ';
-				$values[] = "{$filter->getMaxDate()}000000";
-			}
-			if ($filter->getMinPubdate()) {
-				$search .= 'AND ' . $alias . 'date >= ? ';
-				$values[] = $filter->getMinPubdate();
-			}
-			if ($filter->getMaxPubdate()) {
-				$search .= 'AND ' . $alias . 'date <= ? ';
-				$values[] = $filter->getMaxPubdate();
-			}
+		if ($filters && count($filters->searches()) > 0) {
+			$isOpen = false;
+			foreach ($filters->searches() as $filter) {
+				if ($filter == null) {
+					continue;
+				}
+				$sub_search = '';
+				if ($filter->getMinDate()) {
+					$sub_search .= 'AND ' . $alias . 'id >= ? ';
+					$values[] = "{$filter->getMinDate()}000000";
+				}
+				if ($filter->getMaxDate()) {
+					$sub_search .= 'AND ' . $alias . 'id <= ? ';
+					$values[] = "{$filter->getMaxDate()}000000";
+				}
+				if ($filter->getMinPubdate()) {
+					$sub_search .= 'AND ' . $alias . 'date >= ? ';
+					$values[] = $filter->getMinPubdate();
+				}
+				if ($filter->getMaxPubdate()) {
+					$sub_search .= 'AND ' . $alias . 'date <= ? ';
+					$values[] = $filter->getMaxPubdate();
+				}
 
-			if ($filter->getAuthor()) {
-				foreach ($filter->getAuthor() as $author) {
-					$search .= 'AND ' . $alias . 'author LIKE ? ';
-					$values[] = "%{$author}%";
+				if ($filter->getAuthor()) {
+					foreach ($filter->getAuthor() as $author) {
+						$sub_search .= 'AND ' . $alias . 'author LIKE ? ';
+						$values[] = "%{$author}%";
+					}
 				}
-			}
-			if ($filter->getIntitle()) {
-				foreach ($filter->getIntitle() as $title) {
-					$search .= 'AND ' . $alias . 'title LIKE ? ';
-					$values[] = "%{$title}%";
+				if ($filter->getIntitle()) {
+					foreach ($filter->getIntitle() as $title) {
+						$sub_search .= 'AND ' . $alias . 'title LIKE ? ';
+						$values[] = "%{$title}%";
+					}
 				}
-			}
-			if ($filter->getTags()) {
-				foreach ($filter->getTags() as $tag) {
-					$search .= 'AND ' . $alias . 'tags LIKE ? ';
-					$values[] = "%{$tag}%";
+				if ($filter->getTags()) {
+					foreach ($filter->getTags() as $tag) {
+						$sub_search .= 'AND ' . $alias . 'tags LIKE ? ';
+						$values[] = "%{$tag}%";
+					}
 				}
-			}
-			if ($filter->getInurl()) {
-				foreach ($filter->getInurl() as $url) {
-					$search .= 'AND CONCAT(' . $alias . 'link, ' . $alias . 'guid) LIKE ? ';
-					$values[] = "%{$url}%";
+				if ($filter->getInurl()) {
+					foreach ($filter->getInurl() as $url) {
+						$sub_search .= 'AND CONCAT(' . $alias . 'link, ' . $alias . 'guid) LIKE ? ';
+						$values[] = "%{$url}%";
+					}
 				}
-			}
 
-			if ($filter->getNotAuthor()) {
-				foreach ($filter->getNotAuthor() as $author) {
-					$search .= 'AND (NOT ' . $alias . 'author LIKE ?) ';
-					$values[] = "%{$author}%";
+				if ($filter->getNotAuthor()) {
+					foreach ($filter->getNotAuthor() as $author) {
+						$sub_search .= 'AND (NOT ' . $alias . 'author LIKE ?) ';
+						$values[] = "%{$author}%";
+					}
 				}
-			}
-			if ($filter->getNotIntitle()) {
-				foreach ($filter->getNotIntitle() as $title) {
-					$search .= 'AND (NOT ' . $alias . 'title LIKE ?) ';
-					$values[] = "%{$title}%";
+				if ($filter->getNotIntitle()) {
+					foreach ($filter->getNotIntitle() as $title) {
+						$sub_search .= 'AND (NOT ' . $alias . 'title LIKE ?) ';
+						$values[] = "%{$title}%";
+					}
 				}
-			}
-			if ($filter->getNotTags()) {
-				foreach ($filter->getNotTags() as $tag) {
-					$search .= 'AND (NOT ' . $alias . 'tags LIKE ?) ';
-					$values[] = "%{$tag}%";
+				if ($filter->getNotTags()) {
+					foreach ($filter->getNotTags() as $tag) {
+						$sub_search .= 'AND (NOT ' . $alias . 'tags LIKE ?) ';
+						$values[] = "%{$tag}%";
+					}
 				}
-			}
-			if ($filter->getNotInurl()) {
-				foreach ($filter->getNotInurl() as $url) {
-					$search .= 'AND (NOT CONCAT(' . $alias . 'link, ' . $alias . 'guid) LIKE ?) ';
-					$values[] = "%{$url}%";
+				if ($filter->getNotInurl()) {
+					foreach ($filter->getNotInurl() as $url) {
+						$sub_search .= 'AND (NOT CONCAT(' . $alias . 'link, ' . $alias . 'guid) LIKE ?) ';
+						$values[] = "%{$url}%";
+					}
 				}
-			}
 
-			if ($filter->getSearch()) {
-				foreach ($filter->getSearch() as $search_value) {
-					$search .= 'AND ' . $this->sqlconcat($alias . 'title', $this->isCompressed() ? 'UNCOMPRESS(' . $alias . 'content_bin)' : '' . $alias . 'content') . ' LIKE ? ';
-					$values[] = "%{$search_value}%";
+				if ($filter->getSearch()) {
+					foreach ($filter->getSearch() as $search_value) {
+						$sub_search .= 'AND ' . $this->sqlconcat($alias . 'title', $this->isCompressed() ? 'UNCOMPRESS(' . $alias . 'content_bin)' : '' . $alias . 'content') . ' LIKE ? ';
+						$values[] = "%{$search_value}%";
+					}
 				}
-			}
-			if ($filter->getNotSearch()) {
-				foreach ($filter->getNotSearch() as $search_value) {
-					$search .= 'AND (NOT ' . $this->sqlconcat($alias . 'title', $this->isCompressed() ? 'UNCOMPRESS(' . $alias . 'content_bin)' : '' . $alias . 'content') . ' LIKE ?) ';
-					$values[] = "%{$search_value}%";
+				if ($filter->getNotSearch()) {
+					foreach ($filter->getNotSearch() as $search_value) {
+						$sub_search .= 'AND (NOT ' . $this->sqlconcat($alias . 'title', $this->isCompressed() ? 'UNCOMPRESS(' . $alias . 'content_bin)' : '' . $alias . 'content') . ' LIKE ?) ';
+						$values[] = "%{$search_value}%";
+					}
 				}
+
+				if ($sub_search != '') {
+					if ($isOpen) {
+						$search .= 'OR ';
+					} else {
+						$search .= 'AND (';
+						$isOpen = true;
+					}
+					$search .= '(' . substr($sub_search, 4) . ') ';
+				}
+			}
+			if ($isOpen) {
+				$search .= ') ';
 			}
 		}
 		return array($values, $search);
 	}
 
-	private function sqlListWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0) {
+	private function sqlListWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filters = null, $date_min = 0) {
 		if (!$state) {
 			$state = FreshRSS_Entry::STATE_ALL;
 		}
@@ -777,7 +793,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			throw new FreshRSS_EntriesGetter_Exception('Bad type in Entry->listByType: [' . $type . ']!');
 		}
 
-		list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filter, $state, $order, $firstId, $date_min);
+		list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state, $order, $firstId, $date_min);
 
 		return array(array_merge($values, $searchValues),
 			'SELECT e.id FROM `' . $this->prefix . 'entry` e '
@@ -788,8 +804,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			. ($limit > 0 ? ' LIMIT ' . $limit : ''));	//TODO: See http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/
 	}
 
-	public function listWhereRaw($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0) {
-		list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filter, $date_min);
+	public function listWhereRaw($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filters = null, $date_min = 0) {
+		list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters, $date_min);
 
 		$sql = 'SELECT e0.id, e0.guid, e0.title, e0.author, '
 			. ($this->isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
@@ -805,8 +821,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		return $stm;
 	}
 
-	public function listWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0) {
-		$stm = $this->listWhereRaw($type, $id, $state, $order, $limit, $firstId, $filter, $date_min);
+	public function listWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filters = null, $date_min = 0) {
+		$stm = $this->listWhereRaw($type, $id, $state, $order, $limit, $firstId, $filters, $date_min);
 		return self::daoToEntries($stm->fetchAll(PDO::FETCH_ASSOC));
 	}
 
@@ -827,8 +843,8 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		return self::daoToEntries($stm->fetchAll(PDO::FETCH_ASSOC));
 	}
 
-	public function listIdsWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filter = '', $date_min = 0) {	//For API
-		list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filter, $date_min);
+	public function listIdsWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, $order = 'DESC', $limit = 1, $firstId = '', $filters = null, $date_min = 0) {	//For API
+		list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters, $date_min);
 
 		$stm = $this->bd->prepare($sql);
 		$stm->execute($values);

+ 4 - 4
app/Models/EntryDAOSQLite.php

@@ -159,7 +159,7 @@ DROP TABLE IF EXISTS `tmp`;
 	 * @param integer $priorityMin
 	 * @return integer affected rows
 	 */
-	public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0, $filter = null, $state = 0) {
+	public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0, $filters = null, $state = 0) {
 		if ($idMax == 0) {
 			$idMax = time() . '000000';
 			Minz_Log::debug('Calling markReadEntries(0) is deprecated!');
@@ -173,7 +173,7 @@ DROP TABLE IF EXISTS `tmp`;
 		}
 		$values = array($idMax);
 
-		list($searchValues, $search) = $this->sqlListEntriesWhere('', $filter, $state);
+		list($searchValues, $search) = $this->sqlListEntriesWhere('', $filters, $state);
 
 		$stm = $this->bd->prepare($sql . $search);
 		if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
@@ -199,7 +199,7 @@ DROP TABLE IF EXISTS `tmp`;
 	 * @param integer $idMax fail safe article ID
 	 * @return integer affected rows
 	 */
-	public function markReadCat($id, $idMax = 0, $filter = null, $state = 0) {
+	public function markReadCat($id, $idMax = 0, $filters = null, $state = 0) {
 		if ($idMax == 0) {
 			$idMax = time() . '000000';
 			Minz_Log::debug('Calling markReadCat(0) is deprecated!');
@@ -211,7 +211,7 @@ DROP TABLE IF EXISTS `tmp`;
 			 . 'id_feed IN (SELECT f.id FROM `' . $this->prefix . 'feed` f WHERE f.category=?)';
 		$values = array($idMax, $id);
 
-		list($searchValues, $search) = $this->sqlListEntriesWhere('', $filter, $state);
+		list($searchValues, $search) = $this->sqlListEntriesWhere('', $filters, $state);
 
 		$stm = $this->bd->prepare($sql . $search);
 		if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {

+ 1 - 1
app/Models/UserQuery.php

@@ -41,7 +41,7 @@ class FreshRSS_UserQuery {
 			$query['search'] = '';
 		}
 		// linked to deeply with the search object, need to use dependency injection
-		$this->search = new FreshRSS_Search($query['search']);
+		$this->search = new FreshRSS_BooleanSearch($query['search']);
 		if (isset($query['state'])) {
 			$this->state = $query['state'];
 		}

+ 3 - 0
docs/en/users/03_Main_view.md

@@ -181,3 +181,6 @@ Some operators can be used negatively, to exclude articles, with the same syntax
 `-author:name`, `-intitle:keyword`, `-inurl:keyword`, `-#tag`, `!keyword`.
 
 It is also possible to combine operators to have a very sharp filter, and it is allowed to have multiple instances of `author:`, `intitle:`, `inurl:`, `#`, and free-text.
+
+Combining several search criteria implies a logical *and*, but the keyword ` OR ` can be used to combine several search criteria with a logical *or* instead:
+`author:Dupont OR author:Dupond`

+ 4 - 1
docs/fr/users/03_Main_view.md

@@ -180,4 +180,7 @@ Attention à ne pas introduire d’espace entre l’opérateur et la valeur rech
 Certains opérateurs peuvent être utilisé négativement, pour exclure des articles, avec la même syntaxe que ci-dessus, mais préfixé par `!` ou `-` :
 `-author:nom`, `-intitle:mot`, `-inurl:mot`, `-#tag`, `!mot`.
 
-Il est également possible de combiner les mots-clefs pour faire un filtrage encore plus précis, and et il est autorisé d’avoir plusieurs instances de : `author:`, `intitle:`, `inurl:`, `#`, et texte libre.
+Il est également possible de combiner les mots-clefs pour faire un filtrage encore plus précis, et il est autorisé d’avoir plusieurs instances de : `author:`, `intitle:`, `inurl:`, `#`, et texte libre.
+
+Combiner plusieurs critères implique un *et* logique, mais le mot clef ` OR ` peut être utiliser pour combiner plusieurs critères avec un *ou* logique :
+`author:Dupont OR author:Dupond`

+ 2 - 2
p/api/greader.php

@@ -535,7 +535,7 @@ function streamContents($path, $include_target, $start_time, $count, $order, $ex
 	}
 
 	$entryDAO = FreshRSS_Factory::createEntryDao();
-	$entries = $entryDAO->listWhere($type, $include_target, $state, $order === 'o' ? 'ASC' : 'DESC', $count, $continuation, new FreshRSS_Search(''), $start_time);
+	$entries = $entryDAO->listWhere($type, $include_target, $state, $order === 'o' ? 'ASC' : 'DESC', $count, $continuation, new FreshRSS_BooleanSearch(''), $start_time);
 
 	$items = entriesToArray($entries);
 
@@ -595,7 +595,7 @@ function streamContentsItemsIds($streamId, $start_time, $count, $order, $exclude
 	}
 
 	$entryDAO = FreshRSS_Factory::createEntryDao();
-	$ids = $entryDAO->listIdsWhere($type, $id, $state, $order === 'o' ? 'ASC' : 'DESC', $count, $continuation, new FreshRSS_Search(''), $start_time);
+	$ids = $entryDAO->listIdsWhere($type, $id, $state, $order === 'o' ? 'ASC' : 'DESC', $count, $continuation, new FreshRSS_BooleanSearch(''), $start_time);
 
 	if ($continuation != '') {
 		array_shift($ids);	//Discard first element that was already sent in the previous response