Dan Brown

Added tag searching to search interfaces

...@@ -157,9 +157,13 @@ class Entity extends Ownable ...@@ -157,9 +157,13 @@ class Entity extends Ownable
157 * @param string[] array $wheres 157 * @param string[] array $wheres
158 * @return mixed 158 * @return mixed
159 */ 159 */
160 - public static function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = []) 160 + public function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
161 { 161 {
162 $exactTerms = []; 162 $exactTerms = [];
163 + if (count($terms) === 0) {
164 + $search = $this;
165 + $orderBy = 'updated_at';
166 + } else {
163 foreach ($terms as $key => $term) { 167 foreach ($terms as $key => $term) {
164 $term = htmlentities($term, ENT_QUOTES); 168 $term = htmlentities($term, ENT_QUOTES);
165 $term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term); 169 $term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
...@@ -186,19 +190,21 @@ class Entity extends Ownable ...@@ -186,19 +190,21 @@ class Entity extends Ownable
186 } 190 }
187 }); 191 });
188 } 192 }
193 + $orderBy = 'title_relevance';
194 + };
189 195
190 // Add additional where terms 196 // Add additional where terms
191 foreach ($wheres as $whereTerm) { 197 foreach ($wheres as $whereTerm) {
192 $search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]); 198 $search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]);
193 } 199 }
194 // Load in relations 200 // Load in relations
195 - if (static::isA('page')) { 201 + if ($this->isA('page')) {
196 $search = $search->with('book', 'chapter', 'createdBy', 'updatedBy'); 202 $search = $search->with('book', 'chapter', 'createdBy', 'updatedBy');
197 - } else if (static::isA('chapter')) { 203 + } else if ($this->isA('chapter')) {
198 $search = $search->with('book'); 204 $search = $search->with('book');
199 } 205 }
200 206
201 - return $search->orderBy('title_relevance', 'desc'); 207 + return $search->orderBy($orderBy, 'desc');
202 } 208 }
203 209
204 } 210 }
......
...@@ -286,8 +286,9 @@ class BookRepo extends EntityRepo ...@@ -286,8 +286,9 @@ class BookRepo extends EntityRepo
286 public function getBySearch($term, $count = 20, $paginationAppends = []) 286 public function getBySearch($term, $count = 20, $paginationAppends = [])
287 { 287 {
288 $terms = $this->prepareSearchTerms($term); 288 $terms = $this->prepareSearchTerms($term);
289 - $books = $this->permissionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms)) 289 + $bookQuery = $this->permissionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms));
290 - ->paginate($count)->appends($paginationAppends); 290 + $bookQuery = $this->addAdvancedSearchQueries($bookQuery, $term);
291 + $books = $bookQuery->paginate($count)->appends($paginationAppends);
291 $words = join('|', explode(' ', preg_quote(trim($term), '/'))); 292 $words = join('|', explode(' ', preg_quote(trim($term), '/')));
292 foreach ($books as $book) { 293 foreach ($books as $book) {
293 //highlight 294 //highlight
......
...@@ -168,8 +168,9 @@ class ChapterRepo extends EntityRepo ...@@ -168,8 +168,9 @@ class ChapterRepo extends EntityRepo
168 public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = []) 168 public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
169 { 169 {
170 $terms = $this->prepareSearchTerms($term); 170 $terms = $this->prepareSearchTerms($term);
171 - $chapters = $this->permissionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms)) 171 + $chapterQuery = $this->permissionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms));
172 - ->paginate($count)->appends($paginationAppends); 172 + $chapterQuery = $this->addAdvancedSearchQueries($chapterQuery, $term);
173 + $chapters = $chapterQuery->paginate($count)->appends($paginationAppends);
173 $words = join('|', explode(' ', preg_quote(trim($term), '/'))); 174 $words = join('|', explode(' ', preg_quote(trim($term), '/')));
174 foreach ($chapters as $chapter) { 175 foreach ($chapters as $chapter) {
175 //highlight 176 //highlight
......
...@@ -6,6 +6,7 @@ use BookStack\Entity; ...@@ -6,6 +6,7 @@ use BookStack\Entity;
6 use BookStack\Page; 6 use BookStack\Page;
7 use BookStack\Services\PermissionService; 7 use BookStack\Services\PermissionService;
8 use BookStack\User; 8 use BookStack\User;
9 +use Illuminate\Support\Facades\Log;
9 10
10 class EntityRepo 11 class EntityRepo
11 { 12 {
...@@ -31,6 +32,12 @@ class EntityRepo ...@@ -31,6 +32,12 @@ class EntityRepo
31 protected $permissionService; 32 protected $permissionService;
32 33
33 /** 34 /**
35 + * Acceptable operators to be used in a query
36 + * @var array
37 + */
38 + protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
39 +
40 + /**
34 * EntityService constructor. 41 * EntityService constructor.
35 */ 42 */
36 public function __construct() 43 public function __construct()
...@@ -163,6 +170,7 @@ class EntityRepo ...@@ -163,6 +170,7 @@ class EntityRepo
163 */ 170 */
164 protected function prepareSearchTerms($termString) 171 protected function prepareSearchTerms($termString)
165 { 172 {
173 + $termString = $this->cleanSearchTermString($termString);
166 preg_match_all('/"(.*?)"/', $termString, $matches); 174 preg_match_all('/"(.*?)"/', $termString, $matches);
167 if (count($matches[1]) > 0) { 175 if (count($matches[1]) > 0) {
168 $terms = $matches[1]; 176 $terms = $matches[1];
...@@ -174,5 +182,93 @@ class EntityRepo ...@@ -174,5 +182,93 @@ class EntityRepo
174 return $terms; 182 return $terms;
175 } 183 }
176 184
185 + /**
186 + * Removes any special search notation that should not
187 + * be used in a full-text search.
188 + * @param $termString
189 + * @return mixed
190 + */
191 + protected function cleanSearchTermString($termString)
192 + {
193 + // Strip tag searches
194 + $termString = preg_replace('/\[.*?\]/', '', $termString);
195 + // Reduced multiple spacing into single spacing
196 + $termString = preg_replace("/\s{2,}/", " ", $termString);
197 + return $termString;
198 + }
199 +
200 + /**
201 + * Get the available query operators as a regex escaped list.
202 + * @return mixed
203 + */
204 + protected function getRegexEscapedOperators()
205 + {
206 + $escapedOperators = [];
207 + foreach ($this->queryOperators as $operator) {
208 + $escapedOperators[] = preg_quote($operator);
209 + }
210 + return join('|', $escapedOperators);
211 + }
212 +
213 + /**
214 + * Parses advanced search notations and adds them to the db query.
215 + * @param $query
216 + * @param $termString
217 + * @return mixed
218 + */
219 + protected function addAdvancedSearchQueries($query, $termString)
220 + {
221 + $escapedOperators = $this->getRegexEscapedOperators();
222 + // Look for tag searches
223 + preg_match_all("/\[(.*?)((${escapedOperators})(.*?))?\]/", $termString, $tags);
224 + if (count($tags[0]) > 0) {
225 + $this->applyTagSearches($query, $tags);
226 + }
227 +
228 + return $query;
229 + }
230 +
231 + /**
232 + * Apply extracted tag search terms onto a entity query.
233 + * @param $query
234 + * @param $tags
235 + * @return mixed
236 + */
237 + protected function applyTagSearches($query, $tags) {
238 + $query->where(function($query) use ($tags) {
239 + foreach ($tags[1] as $index => $tagName) {
240 + $query->whereHas('tags', function($query) use ($tags, $index, $tagName) {
241 + $tagOperator = $tags[3][$index];
242 + $tagValue = $tags[4][$index];
243 + if (!empty($tagOperator) && !empty($tagValue) && in_array($tagOperator, $this->queryOperators)) {
244 + if (is_numeric($tagValue) && $tagOperator !== 'like') {
245 + // We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will
246 + // search the value as a string which prevents being able to do number-based operations
247 + // on the tag values. We ensure it has a numeric value and then cast it just to be sure.
248 + $tagValue = (float) trim($query->getConnection()->getPdo()->quote($tagValue), "'");
249 + $query->where('name', '=', $tagName)->whereRaw("value ${tagOperator} ${tagValue}");
250 + } else {
251 + $query->where('name', '=', $tagName)->where('value', $tagOperator, $tagValue);
252 + }
253 + } else {
254 + $query->where('name', '=', $tagName);
255 + }
256 + });
257 + }
258 + });
259 + return $query;
260 + }
177 261
178 } 262 }
263 +
264 +
265 +
266 +
267 +
268 +
269 +
270 +
271 +
272 +
273 +
274 +
......
...@@ -245,8 +245,9 @@ class PageRepo extends EntityRepo ...@@ -245,8 +245,9 @@ class PageRepo extends EntityRepo
245 public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = []) 245 public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
246 { 246 {
247 $terms = $this->prepareSearchTerms($term); 247 $terms = $this->prepareSearchTerms($term);
248 - $pages = $this->permissionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms)) 248 + $pageQuery = $this->permissionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms));
249 - ->paginate($count)->appends($paginationAppends); 249 + $pageQuery = $this->addAdvancedSearchQueries($pageQuery, $term);
250 + $pages = $pageQuery->paginate($count)->appends($paginationAppends);
250 251
251 // Add highlights to page text. 252 // Add highlights to page text.
252 $words = join('|', explode(' ', preg_quote(trim($term), '/'))); 253 $words = join('|', explode(' ', preg_quote(trim($term), '/')));
......
...@@ -8,8 +8,8 @@ ...@@ -8,8 +8,8 @@
8 <table> 8 <table>
9 @foreach($page->tags as $tag) 9 @foreach($page->tags as $tag)
10 <tr class="tag"> 10 <tr class="tag">
11 - <td @if(!$tag->value) colspan="2" @endif> {{ $tag->name }}</td> 11 + <td @if(!$tag->value) colspan="2" @endif><a href="/search/all?term=%5B{{ urlencode($tag->name) }}%5D">{{ $tag->name }}</a></td>
12 - @if($tag->value) <td class="tag-value">{{$tag->value}}</td> @endif 12 + @if($tag->value) <td class="tag-value"><a href="/search/all?term=%5B{{ urlencode($tag->name) }}%3D{{ urlencode($tag->value) }}%5D">{{$tag->value}}</a></td> @endif
13 </tr> 13 </tr>
14 @endforeach 14 @endforeach
15 </table> 15 </table>
......