* @param string[] array $wheres
* @return mixed
*/
- public static function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
+ public function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
{
$exactTerms = [];
- foreach ($terms as $key => $term) {
- $term = htmlentities($term, ENT_QUOTES);
- $term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
- if (preg_match('/\s/', $term)) {
- $exactTerms[] = '%' . $term . '%';
- $term = '"' . $term . '"';
- } else {
- $term = '' . $term . '*';
+ if (count($terms) === 0) {
+ $search = $this;
+ $orderBy = 'updated_at';
+ } else {
+ foreach ($terms as $key => $term) {
+ $term = htmlentities($term, ENT_QUOTES);
+ $term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
+ if (preg_match('/\s/', $term)) {
+ $exactTerms[] = '%' . $term . '%';
+ $term = '"' . $term . '"';
+ } else {
+ $term = '' . $term . '*';
+ }
+ if ($term !== '*') $terms[$key] = $term;
}
- if ($term !== '*') $terms[$key] = $term;
- }
- $termString = implode(' ', $terms);
- $fields = implode(',', $fieldsToSearch);
- $search = static::selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]);
- $search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
-
- // Ensure at least one exact term matches if in search
- if (count($exactTerms) > 0) {
- $search = $search->where(function ($query) use ($exactTerms, $fieldsToSearch) {
- foreach ($exactTerms as $exactTerm) {
- foreach ($fieldsToSearch as $field) {
- $query->orWhere($field, 'like', $exactTerm);
+ $termString = implode(' ', $terms);
+ $fields = implode(',', $fieldsToSearch);
+ $search = static::selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]);
+ $search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
+
+ // Ensure at least one exact term matches if in search
+ if (count($exactTerms) > 0) {
+ $search = $search->where(function ($query) use ($exactTerms, $fieldsToSearch) {
+ foreach ($exactTerms as $exactTerm) {
+ foreach ($fieldsToSearch as $field) {
+ $query->orWhere($field, 'like', $exactTerm);
+ }
}
- }
- });
- }
+ });
+ }
+ $orderBy = 'title_relevance';
+ };
// Add additional where terms
foreach ($wheres as $whereTerm) {
$search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]);
}
// Load in relations
- if (static::isA('page')) {
+ if ($this->isA('page')) {
$search = $search->with('book', 'chapter', 'createdBy', 'updatedBy');
- } else if (static::isA('chapter')) {
+ } else if ($this->isA('chapter')) {
$search = $search->with('book');
}
- return $search->orderBy('title_relevance', 'desc');
+ return $search->orderBy($orderBy, 'desc');
}
}
public function getBySearch($term, $count = 20, $paginationAppends = [])
{
$terms = $this->prepareSearchTerms($term);
- $books = $this->permissionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms))
- ->paginate($count)->appends($paginationAppends);
+ $bookQuery = $this->permissionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms));
+ $bookQuery = $this->addAdvancedSearchQueries($bookQuery, $term);
+ $books = $bookQuery->paginate($count)->appends($paginationAppends);
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
foreach ($books as $book) {
//highlight
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
{
$terms = $this->prepareSearchTerms($term);
- $chapters = $this->permissionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms))
- ->paginate($count)->appends($paginationAppends);
+ $chapterQuery = $this->permissionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms));
+ $chapterQuery = $this->addAdvancedSearchQueries($chapterQuery, $term);
+ $chapters = $chapterQuery->paginate($count)->appends($paginationAppends);
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
foreach ($chapters as $chapter) {
//highlight
use BookStack\Page;
use BookStack\Services\PermissionService;
use BookStack\User;
+use Illuminate\Support\Facades\Log;
class EntityRepo
{
*/
protected $permissionService;
+ /**
+ * Acceptable operators to be used in a query
+ * @var array
+ */
+ protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
+
/**
* EntityService constructor.
*/
*/
protected function prepareSearchTerms($termString)
{
+ $termString = $this->cleanSearchTermString($termString);
preg_match_all('/"(.*?)"/', $termString, $matches);
if (count($matches[1]) > 0) {
$terms = $matches[1];
return $terms;
}
+ /**
+ * Removes any special search notation that should not
+ * be used in a full-text search.
+ * @param $termString
+ * @return mixed
+ */
+ protected function cleanSearchTermString($termString)
+ {
+ // Strip tag searches
+ $termString = preg_replace('/\[.*?\]/', '', $termString);
+ // Reduced multiple spacing into single spacing
+ $termString = preg_replace("/\s{2,}/", " ", $termString);
+ return $termString;
+ }
+
+ /**
+ * Get the available query operators as a regex escaped list.
+ * @return mixed
+ */
+ protected function getRegexEscapedOperators()
+ {
+ $escapedOperators = [];
+ foreach ($this->queryOperators as $operator) {
+ $escapedOperators[] = preg_quote($operator);
+ }
+ return join('|', $escapedOperators);
+ }
+
+ /**
+ * Parses advanced search notations and adds them to the db query.
+ * @param $query
+ * @param $termString
+ * @return mixed
+ */
+ protected function addAdvancedSearchQueries($query, $termString)
+ {
+ $escapedOperators = $this->getRegexEscapedOperators();
+ // Look for tag searches
+ preg_match_all("/\[(.*?)((${escapedOperators})(.*?))?\]/", $termString, $tags);
+ if (count($tags[0]) > 0) {
+ $this->applyTagSearches($query, $tags);
+ }
+
+ return $query;
+ }
+
+ /**
+ * Apply extracted tag search terms onto a entity query.
+ * @param $query
+ * @param $tags
+ * @return mixed
+ */
+ protected function applyTagSearches($query, $tags) {
+ $query->where(function($query) use ($tags) {
+ foreach ($tags[1] as $index => $tagName) {
+ $query->whereHas('tags', function($query) use ($tags, $index, $tagName) {
+ $tagOperator = $tags[3][$index];
+ $tagValue = $tags[4][$index];
+ if (!empty($tagOperator) && !empty($tagValue) && in_array($tagOperator, $this->queryOperators)) {
+ if (is_numeric($tagValue) && $tagOperator !== 'like') {
+ // We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will
+ // search the value as a string which prevents being able to do number-based operations
+ // on the tag values. We ensure it has a numeric value and then cast it just to be sure.
+ $tagValue = (float) trim($query->getConnection()->getPdo()->quote($tagValue), "'");
+ $query->where('name', '=', $tagName)->whereRaw("value ${tagOperator} ${tagValue}");
+ } else {
+ $query->where('name', '=', $tagName)->where('value', $tagOperator, $tagValue);
+ }
+ } else {
+ $query->where('name', '=', $tagName);
+ }
+ });
+ }
+ });
+ return $query;
+ }
+
+}
+
+
+
+
+
+
+
+
+
+
+
-}
\ No newline at end of file
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
{
$terms = $this->prepareSearchTerms($term);
- $pages = $this->permissionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms))
- ->paginate($count)->appends($paginationAppends);
+ $pageQuery = $this->permissionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms));
+ $pageQuery = $this->addAdvancedSearchQueries($pageQuery, $term);
+ $pages = $pageQuery->paginate($count)->appends($paginationAppends);
// Add highlights to page text.
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
<table>
@foreach($page->tags as $tag)
<tr class="tag">
- <td @if(!$tag->value) colspan="2" @endif> {{ $tag->name }}</td>
- @if($tag->value) <td class="tag-value">{{$tag->value}}</td> @endif
+ <td @if(!$tag->value) colspan="2" @endif><a href="/search/all?term=%5B{{ urlencode($tag->name) }}%5D">{{ $tag->name }}</a></td>
+ @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
</tr>
@endforeach
</table>