mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
Added tag searches and advanced filters to new search
This commit is contained in:
parent
331305333d
commit
01cb22af37
@ -64,12 +64,6 @@ class EntityRepo
|
|||||||
*/
|
*/
|
||||||
protected $searchService;
|
protected $searchService;
|
||||||
|
|
||||||
/**
|
|
||||||
* Acceptable operators to be used in a query
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EntityRepo constructor.
|
* EntityRepo constructor.
|
||||||
* @param Book $book
|
* @param Book $book
|
||||||
@ -370,56 +364,6 @@ class EntityRepo
|
|||||||
->orderBy('draft', 'DESC')->orderBy('priority', 'ASC')->get();
|
->orderBy('draft', 'DESC')->orderBy('priority', 'ASC')->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Search entities of a type via a given query.
|
|
||||||
* @param string $type
|
|
||||||
* @param string $term
|
|
||||||
* @param array $whereTerms
|
|
||||||
* @param int $count
|
|
||||||
* @param array $paginationAppends
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function getBySearch($type, $term, $whereTerms = [], $count = 20, $paginationAppends = [])
|
|
||||||
{
|
|
||||||
$terms = $this->prepareSearchTerms($term);
|
|
||||||
$q = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type)->fullTextSearchQuery($terms, $whereTerms));
|
|
||||||
$q = $this->addAdvancedSearchQueries($q, $term);
|
|
||||||
$entities = $q->paginate($count)->appends($paginationAppends);
|
|
||||||
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
|
|
||||||
|
|
||||||
// Highlight page content
|
|
||||||
if ($type === 'page') {
|
|
||||||
//lookahead/behind assertions ensures cut between words
|
|
||||||
$s = '\s\x00-/:-@\[-`{-~'; //character set for start/end of words
|
|
||||||
|
|
||||||
foreach ($entities as $page) {
|
|
||||||
preg_match_all('#(?<=[' . $s . ']).{1,30}((' . $words . ').{1,30})+(?=[' . $s . '])#uis', $page->text, $matches, PREG_SET_ORDER);
|
|
||||||
//delimiter between occurrences
|
|
||||||
$results = [];
|
|
||||||
foreach ($matches as $line) {
|
|
||||||
$results[] = htmlspecialchars($line[0], 0, 'UTF-8');
|
|
||||||
}
|
|
||||||
$matchLimit = 6;
|
|
||||||
if (count($results) > $matchLimit) $results = array_slice($results, 0, $matchLimit);
|
|
||||||
$result = join('... ', $results);
|
|
||||||
|
|
||||||
//highlight
|
|
||||||
$result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $result);
|
|
||||||
if (strlen($result) < 5) $result = $page->getExcerpt(80);
|
|
||||||
|
|
||||||
$page->searchSnippet = $result;
|
|
||||||
}
|
|
||||||
return $entities;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Highlight chapter/book content
|
|
||||||
foreach ($entities as $entity) {
|
|
||||||
//highlight
|
|
||||||
$result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $entity->getExcerpt(100));
|
|
||||||
$entity->searchSnippet = $result;
|
|
||||||
}
|
|
||||||
return $entities;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the next sequential priority for a new child element in the given book.
|
* Get the next sequential priority for a new child element in the given book.
|
||||||
@ -501,104 +445,7 @@ class EntityRepo
|
|||||||
$this->permissionService->buildJointPermissionsForEntity($entity);
|
$this->permissionService->buildJointPermissionsForEntity($entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepare a string of search terms by turning
|
|
||||||
* it into an array of terms.
|
|
||||||
* Keeps quoted terms together.
|
|
||||||
* @param $termString
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function prepareSearchTerms($termString)
|
|
||||||
{
|
|
||||||
$termString = $this->cleanSearchTermString($termString);
|
|
||||||
preg_match_all('/(".*?")/', $termString, $matches);
|
|
||||||
$terms = [];
|
|
||||||
if (count($matches[1]) > 0) {
|
|
||||||
foreach ($matches[1] as $match) {
|
|
||||||
$terms[] = $match;
|
|
||||||
}
|
|
||||||
$termString = trim(preg_replace('/"(.*?)"/', '', $termString));
|
|
||||||
}
|
|
||||||
if (!empty($termString)) $terms = array_merge($terms, explode(' ', $termString));
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new entity from request input.
|
* Create a new entity from request input.
|
||||||
|
@ -12,7 +12,6 @@ use Illuminate\Support\Collection;
|
|||||||
|
|
||||||
class SearchService
|
class SearchService
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $searchTerm;
|
protected $searchTerm;
|
||||||
protected $book;
|
protected $book;
|
||||||
protected $chapter;
|
protected $chapter;
|
||||||
@ -21,6 +20,12 @@ class SearchService
|
|||||||
protected $permissionService;
|
protected $permissionService;
|
||||||
protected $entities;
|
protected $entities;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acceptable operators to be used in a query
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SearchService constructor.
|
* SearchService constructor.
|
||||||
* @param SearchTerm $searchTerm
|
* @param SearchTerm $searchTerm
|
||||||
@ -55,11 +60,7 @@ class SearchService
|
|||||||
*/
|
*/
|
||||||
public function searchEntities($searchString, $entityType = 'all', $page = 0, $count = 20)
|
public function searchEntities($searchString, $entityType = 'all', $page = 0, $count = 20)
|
||||||
{
|
{
|
||||||
// TODO - Add Tag Searches
|
|
||||||
// TODO - Add advanced custom column searches
|
|
||||||
// TODO - Check drafts don't show up in results
|
// TODO - Check drafts don't show up in results
|
||||||
// TODO - Move search all page to just /search?term=cat
|
|
||||||
|
|
||||||
if ($entityType !== 'all') return $this->searchEntityTable($searchString, $entityType, $page, $count);
|
if ($entityType !== 'all') return $this->searchEntityTable($searchString, $entityType, $page, $count);
|
||||||
|
|
||||||
$bookSearch = $this->searchEntityTable($searchString, 'book', $page, $count);
|
$bookSearch = $this->searchEntityTable($searchString, 'book', $page, $count);
|
||||||
@ -109,6 +110,19 @@ class SearchService
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle tag searches
|
||||||
|
foreach ($searchTerms['tags'] as $inputTerm) {
|
||||||
|
$this->applyTagSearch($entitySelect, $inputTerm);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle filters
|
||||||
|
foreach ($searchTerms['filters'] as $filterTerm) {
|
||||||
|
$splitTerm = explode(':', $filterTerm);
|
||||||
|
$functionName = camel_case('filter_' . $splitTerm[0]);
|
||||||
|
$param = count($splitTerm) > 1 ? $splitTerm[1] : '';
|
||||||
|
if (method_exists($this, $functionName)) $this->$functionName($entitySelect, $entity, $param);
|
||||||
|
}
|
||||||
|
|
||||||
$entitySelect->skip($page * $count)->take($count);
|
$entitySelect->skip($page * $count)->take($count);
|
||||||
$query = $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view');
|
$query = $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view');
|
||||||
return $query->get();
|
return $query->get();
|
||||||
@ -120,7 +134,7 @@ class SearchService
|
|||||||
* @param $searchString
|
* @param $searchString
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function parseSearchString($searchString)
|
protected function parseSearchString($searchString)
|
||||||
{
|
{
|
||||||
$terms = [
|
$terms = [
|
||||||
'search' => [],
|
'search' => [],
|
||||||
@ -151,6 +165,50 @@ class SearchService
|
|||||||
return $terms;
|
return $terms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a tag search term onto a entity query.
|
||||||
|
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||||
|
* @param string $tagTerm
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
protected function applyTagSearch(\Illuminate\Database\Eloquent\Builder $query, $tagTerm) {
|
||||||
|
preg_match("/^(.*?)((".$this->getRegexEscapedOperators().")(.*?))?$/", $tagTerm, $tagSplit);
|
||||||
|
$query->whereHas('tags', function(\Illuminate\Database\Eloquent\Builder $query) use ($tagSplit) {
|
||||||
|
$tagName = $tagSplit[1];
|
||||||
|
$tagOperator = count($tagSplit) > 2 ? $tagSplit[3] : '';
|
||||||
|
$tagValue = count($tagSplit) > 3 ? $tagSplit[4] : '';
|
||||||
|
$validOperator = in_array($tagOperator, $this->queryOperators);
|
||||||
|
if (!empty($tagOperator) && !empty($tagValue) && $validOperator) {
|
||||||
|
if (!empty($tagName)) $query->where('name', '=', $tagName);
|
||||||
|
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->whereRaw("value ${tagOperator} ${tagValue}");
|
||||||
|
} else {
|
||||||
|
$query->where('value', $tagOperator, $tagValue);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$query->where('name', '=', $tagName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an entity instance via type.
|
* Get an entity instance via type.
|
||||||
* @param $type
|
* @param $type
|
||||||
@ -258,4 +316,82 @@ class SearchService
|
|||||||
return $terms;
|
return $terms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom entity search filters
|
||||||
|
*/
|
||||||
|
|
||||||
|
protected function filterUpdatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||||
|
{
|
||||||
|
try { $date = date_create($input);
|
||||||
|
} catch (\Exception $e) {return;}
|
||||||
|
$query->where('updated_at', '>=', $date);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function filterUpdatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||||
|
{
|
||||||
|
try { $date = date_create($input);
|
||||||
|
} catch (\Exception $e) {return;}
|
||||||
|
$query->where('updated_at', '<', $date);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function filterCreatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||||
|
{
|
||||||
|
try { $date = date_create($input);
|
||||||
|
} catch (\Exception $e) {return;}
|
||||||
|
$query->where('created_at', '>=', $date);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function filterCreatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||||
|
{
|
||||||
|
try { $date = date_create($input);
|
||||||
|
} catch (\Exception $e) {return;}
|
||||||
|
$query->where('created_at', '<', $date);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function filterCreatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||||
|
{
|
||||||
|
if (!is_numeric($input)) return;
|
||||||
|
$query->where('created_by', '=', $input);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function filterUpdatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||||
|
{
|
||||||
|
if (!is_numeric($input)) return;
|
||||||
|
$query->where('updated_by', '=', $input);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function filterInName(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||||
|
{
|
||||||
|
$query->where('name', 'like', '%' .$input. '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function filterInTitle(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) {$this->filterInName($query, $model, $input);}
|
||||||
|
|
||||||
|
protected function filterInBody(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||||
|
{
|
||||||
|
$query->where($model->textField, 'like', '%' .$input. '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function filterIsRestricted(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||||
|
{
|
||||||
|
$query->where('restricted', '=', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function filterViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||||
|
{
|
||||||
|
$query->whereHas('views', function($query) {
|
||||||
|
$query->where('user_id', '=', user()->id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function filterNotViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||||
|
{
|
||||||
|
$query->whereDoesntHave('views', function($query) {
|
||||||
|
$query->where('user_id', '=', user()->id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -47,7 +47,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-4 col-sm-3 text-center">
|
<div class="col-lg-4 col-sm-3 text-center">
|
||||||
<form action="{{ baseUrl('/search/all') }}" method="GET" class="search-box">
|
<form action="{{ baseUrl('/search') }}" method="GET" class="search-box">
|
||||||
<input id="header-search-box-input" type="text" name="term" tabindex="2" value="{{ isset($searchTerm) ? $searchTerm : '' }}">
|
<input id="header-search-box-input" type="text" name="term" tabindex="2" value="{{ isset($searchTerm) ? $searchTerm : '' }}">
|
||||||
<button id="header-search-box-button" type="submit" class="text-button"><i class="zmdi zmdi-search"></i></button>
|
<button id="header-search-box-button" type="submit" class="text-button"><i class="zmdi zmdi-search"></i></button>
|
||||||
</form>
|
</form>
|
||||||
|
@ -123,7 +123,7 @@ Route::group(['middleware' => 'auth'], function () {
|
|||||||
Route::get('/link/{id}', 'PageController@redirectFromLink');
|
Route::get('/link/{id}', 'PageController@redirectFromLink');
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
Route::get('/search/all', 'SearchController@searchAll');
|
Route::get('/search', 'SearchController@searchAll');
|
||||||
Route::get('/search/pages', 'SearchController@searchPages');
|
Route::get('/search/pages', 'SearchController@searchPages');
|
||||||
Route::get('/search/books', 'SearchController@searchBooks');
|
Route::get('/search/books', 'SearchController@searchBooks');
|
||||||
Route::get('/search/chapters', 'SearchController@searchChapters');
|
Route::get('/search/chapters', 'SearchController@searchChapters');
|
||||||
|
Loading…
Reference in New Issue
Block a user