mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-05-05 08:40:11 +00:00
Added tag searching to search interfaces
This commit is contained in:
parent
7932069535
commit
8d80e7311c
6 changed files with 143 additions and 38 deletions
app
resources/views/pages
|
@ -157,48 +157,54 @@ class Entity extends Ownable
|
||||||
* @param string[] array $wheres
|
* @param string[] array $wheres
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public static function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
|
public function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
|
||||||
{
|
{
|
||||||
$exactTerms = [];
|
$exactTerms = [];
|
||||||
foreach ($terms as $key => $term) {
|
if (count($terms) === 0) {
|
||||||
$term = htmlentities($term, ENT_QUOTES);
|
$search = $this;
|
||||||
$term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
|
$orderBy = 'updated_at';
|
||||||
if (preg_match('/\s/', $term)) {
|
} else {
|
||||||
$exactTerms[] = '%' . $term . '%';
|
foreach ($terms as $key => $term) {
|
||||||
$term = '"' . $term . '"';
|
$term = htmlentities($term, ENT_QUOTES);
|
||||||
} else {
|
$term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
|
||||||
$term = '' . $term . '*';
|
if (preg_match('/\s/', $term)) {
|
||||||
}
|
$exactTerms[] = '%' . $term . '%';
|
||||||
if ($term !== '*') $terms[$key] = $term;
|
$term = '"' . $term . '"';
|
||||||
}
|
} else {
|
||||||
$termString = implode(' ', $terms);
|
$term = '' . $term . '*';
|
||||||
$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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
$orderBy = 'title_relevance';
|
||||||
|
};
|
||||||
|
|
||||||
// Add additional where terms
|
// Add additional where terms
|
||||||
foreach ($wheres as $whereTerm) {
|
foreach ($wheres as $whereTerm) {
|
||||||
$search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]);
|
$search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]);
|
||||||
}
|
}
|
||||||
// Load in relations
|
// Load in relations
|
||||||
if (static::isA('page')) {
|
if ($this->isA('page')) {
|
||||||
$search = $search->with('book', 'chapter', 'createdBy', 'updatedBy');
|
$search = $search->with('book', 'chapter', 'createdBy', 'updatedBy');
|
||||||
} else if (static::isA('chapter')) {
|
} else if ($this->isA('chapter')) {
|
||||||
$search = $search->with('book');
|
$search = $search->with('book');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $search->orderBy('title_relevance', 'desc');
|
return $search->orderBy($orderBy, 'desc');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -286,8 +286,9 @@ class BookRepo extends EntityRepo
|
||||||
public function getBySearch($term, $count = 20, $paginationAppends = [])
|
public function getBySearch($term, $count = 20, $paginationAppends = [])
|
||||||
{
|
{
|
||||||
$terms = $this->prepareSearchTerms($term);
|
$terms = $this->prepareSearchTerms($term);
|
||||||
$books = $this->permissionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms))
|
$bookQuery = $this->permissionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms));
|
||||||
->paginate($count)->appends($paginationAppends);
|
$bookQuery = $this->addAdvancedSearchQueries($bookQuery, $term);
|
||||||
|
$books = $bookQuery->paginate($count)->appends($paginationAppends);
|
||||||
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
|
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
|
||||||
foreach ($books as $book) {
|
foreach ($books as $book) {
|
||||||
//highlight
|
//highlight
|
||||||
|
|
|
@ -168,8 +168,9 @@ class ChapterRepo extends EntityRepo
|
||||||
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
|
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
|
||||||
{
|
{
|
||||||
$terms = $this->prepareSearchTerms($term);
|
$terms = $this->prepareSearchTerms($term);
|
||||||
$chapters = $this->permissionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms))
|
$chapterQuery = $this->permissionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms));
|
||||||
->paginate($count)->appends($paginationAppends);
|
$chapterQuery = $this->addAdvancedSearchQueries($chapterQuery, $term);
|
||||||
|
$chapters = $chapterQuery->paginate($count)->appends($paginationAppends);
|
||||||
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
|
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
|
||||||
foreach ($chapters as $chapter) {
|
foreach ($chapters as $chapter) {
|
||||||
//highlight
|
//highlight
|
||||||
|
|
|
@ -6,6 +6,7 @@ use BookStack\Entity;
|
||||||
use BookStack\Page;
|
use BookStack\Page;
|
||||||
use BookStack\Services\PermissionService;
|
use BookStack\Services\PermissionService;
|
||||||
use BookStack\User;
|
use BookStack\User;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class EntityRepo
|
class EntityRepo
|
||||||
{
|
{
|
||||||
|
@ -30,6 +31,12 @@ class EntityRepo
|
||||||
*/
|
*/
|
||||||
protected $permissionService;
|
protected $permissionService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acceptable operators to be used in a query
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EntityService constructor.
|
* EntityService constructor.
|
||||||
*/
|
*/
|
||||||
|
@ -163,6 +170,7 @@ class EntityRepo
|
||||||
*/
|
*/
|
||||||
protected function prepareSearchTerms($termString)
|
protected function prepareSearchTerms($termString)
|
||||||
{
|
{
|
||||||
|
$termString = $this->cleanSearchTermString($termString);
|
||||||
preg_match_all('/"(.*?)"/', $termString, $matches);
|
preg_match_all('/"(.*?)"/', $termString, $matches);
|
||||||
if (count($matches[1]) > 0) {
|
if (count($matches[1]) > 0) {
|
||||||
$terms = $matches[1];
|
$terms = $matches[1];
|
||||||
|
@ -174,5 +182,93 @@ class EntityRepo
|
||||||
return $terms;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -245,8 +245,9 @@ class PageRepo extends EntityRepo
|
||||||
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
|
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
|
||||||
{
|
{
|
||||||
$terms = $this->prepareSearchTerms($term);
|
$terms = $this->prepareSearchTerms($term);
|
||||||
$pages = $this->permissionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms))
|
$pageQuery = $this->permissionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms));
|
||||||
->paginate($count)->appends($paginationAppends);
|
$pageQuery = $this->addAdvancedSearchQueries($pageQuery, $term);
|
||||||
|
$pages = $pageQuery->paginate($count)->appends($paginationAppends);
|
||||||
|
|
||||||
// Add highlights to page text.
|
// Add highlights to page text.
|
||||||
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
|
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
|
||||||
|
|
|
@ -8,8 +8,8 @@
|
||||||
<table>
|
<table>
|
||||||
@foreach($page->tags as $tag)
|
@foreach($page->tags as $tag)
|
||||||
<tr class="tag">
|
<tr class="tag">
|
||||||
<td @if(!$tag->value) colspan="2" @endif> {{ $tag->name }}</td>
|
<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">{{$tag->value}}</td> @endif
|
@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>
|
</tr>
|
||||||
@endforeach
|
@endforeach
|
||||||
</table>
|
</table>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue