Dan Brown

Added search term parsing and exact term matches

...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
4 class Entity extends Ownable 4 class Entity extends Ownable
5 { 5 {
6 6
7 - protected $textField = 'description'; 7 + public $textField = 'description';
8 8
9 /** 9 /**
10 * Compares this entity to another given entity. 10 * Compares this entity to another given entity.
......
...@@ -8,7 +8,7 @@ class Page extends Entity ...@@ -8,7 +8,7 @@ class Page extends Entity
8 protected $simpleAttributes = ['name', 'id', 'slug']; 8 protected $simpleAttributes = ['name', 'id', 'slug'];
9 9
10 protected $with = ['book']; 10 protected $with = ['book'];
11 - protected $textField = 'text'; 11 + public $textField = 'text';
12 12
13 /** 13 /**
14 * Converts this page into a simplified array. 14 * Converts this page into a simplified array.
......
...@@ -6,7 +6,9 @@ use BookStack\Entity; ...@@ -6,7 +6,9 @@ use BookStack\Entity;
6 use BookStack\Page; 6 use BookStack\Page;
7 use BookStack\SearchTerm; 7 use BookStack\SearchTerm;
8 use Illuminate\Database\Connection; 8 use Illuminate\Database\Connection;
9 +use Illuminate\Database\Query\Builder;
9 use Illuminate\Database\Query\JoinClause; 10 use Illuminate\Database\Query\JoinClause;
11 +use Illuminate\Support\Collection;
10 12
11 class SearchService 13 class SearchService
12 { 14 {
...@@ -43,11 +45,18 @@ class SearchService ...@@ -43,11 +45,18 @@ class SearchService
43 $this->permissionService = $permissionService; 45 $this->permissionService = $permissionService;
44 } 46 }
45 47
48 + /**
49 + * Search all entities in the system.
50 + * @param $searchString
51 + * @param string $entityType
52 + * @param int $page
53 + * @param int $count
54 + * @return Collection
55 + */
46 public function searchEntities($searchString, $entityType = 'all', $page = 0, $count = 20) 56 public function searchEntities($searchString, $entityType = 'all', $page = 0, $count = 20)
47 { 57 {
48 // TODO - Add Tag Searches 58 // TODO - Add Tag Searches
49 // TODO - Add advanced custom column searches 59 // TODO - Add advanced custom column searches
50 - // TODO - Add exact match searches ("")
51 // TODO - Check drafts don't show up in results 60 // TODO - Check drafts don't show up in results
52 // TODO - Move search all page to just /search?term=cat 61 // TODO - Move search all page to just /search?term=cat
53 62
...@@ -59,27 +68,89 @@ class SearchService ...@@ -59,27 +68,89 @@ class SearchService
59 return collect($bookSearch)->merge($chapterSearch)->merge($pageSearch)->sortByDesc('score'); 68 return collect($bookSearch)->merge($chapterSearch)->merge($pageSearch)->sortByDesc('score');
60 } 69 }
61 70
71 + /**
72 + * Search across a particular entity type.
73 + * @param string $searchString
74 + * @param string $entityType
75 + * @param int $page
76 + * @param int $count
77 + * @return \Illuminate\Database\Eloquent\Collection|static[]
78 + */
62 public function searchEntityTable($searchString, $entityType = 'page', $page = 0, $count = 20) 79 public function searchEntityTable($searchString, $entityType = 'page', $page = 0, $count = 20)
63 { 80 {
64 - $termArray = explode(' ', $searchString); 81 + $searchTerms = $this->parseSearchString($searchString);
65 -
66 - $subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score'));
67 - $subQuery->where(function($query) use ($termArray) {
68 - foreach ($termArray as $inputTerm) {
69 - $query->orWhere('term', 'like', $inputTerm .'%');
70 - }
71 - });
72 82
73 $entity = $this->getEntity($entityType); 83 $entity = $this->getEntity($entityType);
74 - $subQuery = $subQuery->groupBy('entity_type', 'entity_id'); 84 + $entitySelect = $entity->newQuery();
75 - $entitySelect = $entity->newQuery()->join(\DB::raw('(' . $subQuery->toSql() . ') as s'), function(JoinClause $join) { 85 +
76 - $join->on('id', '=', 'entity_id'); 86 + // Handle normal search terms
77 - })->selectRaw($entity->getTable().'.*, s.score')->orderBy('score', 'desc')->skip($page * $count)->take($count); 87 + if (count($searchTerms['search']) > 0) {
78 - $entitySelect->mergeBindings($subQuery); 88 + $subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score'));
89 + $subQuery->where(function(Builder $query) use ($searchTerms) {
90 + foreach ($searchTerms['search'] as $inputTerm) {
91 + $query->orWhere('term', 'like', $inputTerm .'%');
92 + }
93 + })->groupBy('entity_type', 'entity_id');
94 + $entitySelect->join(\DB::raw('(' . $subQuery->toSql() . ') as s'), function(JoinClause $join) {
95 + $join->on('id', '=', 'entity_id');
96 + })->selectRaw($entity->getTable().'.*, s.score')->orderBy('score', 'desc');
97 + $entitySelect->mergeBindings($subQuery);
98 + }
99 +
100 + // Handle exact term matching
101 + if (count($searchTerms['exact']) > 0) {
102 + $entitySelect->where(function(\Illuminate\Database\Eloquent\Builder $query) use ($searchTerms, $entity) {
103 + foreach ($searchTerms['exact'] as $inputTerm) {
104 + $query->where(function (\Illuminate\Database\Eloquent\Builder $query) use ($inputTerm, $entity) {
105 + $query->where('name', 'like', '%'.$inputTerm .'%')
106 + ->orWhere($entity->textField, 'like', '%'.$inputTerm .'%');
107 + });
108 + }
109 + });
110 + }
111 +
112 + $entitySelect->skip($page * $count)->take($count);
79 $query = $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view'); 113 $query = $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view');
80 return $query->get(); 114 return $query->get();
81 } 115 }
82 116
117 +
118 + /**
119 + * Parse a search string into components.
120 + * @param $searchString
121 + * @return array
122 + */
123 + public function parseSearchString($searchString)
124 + {
125 + $terms = [
126 + 'search' => [],
127 + 'exact' => [],
128 + 'tags' => [],
129 + 'filters' => []
130 + ];
131 +
132 + $patterns = [
133 + 'exact' => '/"(.*?)"/',
134 + 'tags' => '/\[(.*?)\]/',
135 + 'filters' => '/\{(.*?)\}/'
136 + ];
137 +
138 + foreach ($patterns as $termType => $pattern) {
139 + $matches = [];
140 + preg_match_all($pattern, $searchString, $matches);
141 + if (count($matches) > 0) {
142 + $terms[$termType] = $matches[1];
143 + $searchString = preg_replace($pattern, '', $searchString);
144 + }
145 + }
146 +
147 + foreach (explode(' ', trim($searchString)) as $searchTerm) {
148 + if ($searchTerm !== '') $terms['search'][] = $searchTerm;
149 + }
150 +
151 + return $terms;
152 + }
153 +
83 /** 154 /**
84 * Get an entity instance via type. 155 * Get an entity instance via type.
85 * @param $type 156 * @param $type
......