]> BookStack Code Mirror - bookstack/blob - app/Services/SearchService.php
be1303ca04ac59347cca106df2f5a05a630ba389
[bookstack] / app / Services / SearchService.php
1 <?php namespace BookStack\Services;
2
3 use BookStack\Book;
4 use BookStack\Chapter;
5 use BookStack\Entity;
6 use BookStack\Page;
7 use BookStack\SearchTerm;
8 use Illuminate\Database\Connection;
9 use Illuminate\Database\Query\JoinClause;
10
11 class SearchService
12 {
13
14     protected $searchTerm;
15     protected $book;
16     protected $chapter;
17     protected $page;
18     protected $db;
19
20     /**
21      * SearchService constructor.
22      * @param SearchTerm $searchTerm
23      * @param Book $book
24      * @param Chapter $chapter
25      * @param Page $page
26      * @param Connection $db
27      */
28     public function __construct(SearchTerm $searchTerm, Book $book, Chapter $chapter, Page $page, Connection $db)
29     {
30         $this->searchTerm = $searchTerm;
31         $this->book = $book;
32         $this->chapter = $chapter;
33         $this->page = $page;
34         $this->db = $db;
35     }
36
37     public function searchEntities($searchString, $entityType = 'all')
38     {
39         // TODO - Add Tag Searches
40         // TODO - Add advanced custom column searches
41         // TODO - Add exact match searches ("")
42
43         $termArray = explode(' ', $searchString);
44
45         $subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score'));
46         $subQuery->where(function($query) use ($termArray) {
47             foreach ($termArray as $inputTerm) {
48                 $query->orWhere('term', 'like', $inputTerm .'%');
49             }
50         });
51
52         $subQuery = $subQuery->groupBy('entity_type', 'entity_id');
53         $pageSelect = $this->db->table('pages as e')->join(\DB::raw('(' . $subQuery->toSql() . ') as s'), function(JoinClause $join) {
54             $join->on('e.id', '=', 's.entity_id');
55         })->selectRaw('e.*, s.score')->orderBy('score', 'desc');
56         $pageSelect->mergeBindings($subQuery);
57         dd($pageSelect->toSql());
58         // TODO - Continue from here
59     }
60
61     /**
62      * Index the given entity.
63      * @param Entity $entity
64      */
65     public function indexEntity(Entity $entity)
66     {
67         $this->deleteEntityTerms($entity);
68         $nameTerms = $this->generateTermArrayFromText($entity->name, 5);
69         $bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1);
70         $terms = array_merge($nameTerms, $bodyTerms);
71         $entity->searchTerms()->createMany($terms);
72     }
73
74     /**
75      * Index multiple Entities at once
76      * @param Entity[] $entities
77      */
78     protected function indexEntities($entities) {
79         $terms = [];
80         foreach ($entities as $entity) {
81             $nameTerms = $this->generateTermArrayFromText($entity->name, 5);
82             $bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1);
83             foreach (array_merge($nameTerms, $bodyTerms) as $term) {
84                 $term['entity_id'] = $entity->id;
85                 $term['entity_type'] = $entity->getMorphClass();
86                 $terms[] = $term;
87             }
88         }
89         $this->searchTerm->insert($terms);
90     }
91
92     /**
93      * Delete and re-index the terms for all entities in the system.
94      */
95     public function indexAllEntities()
96     {
97         $this->searchTerm->truncate();
98
99         // Chunk through all books
100         $this->book->chunk(500, function ($books) {
101             $this->indexEntities($books);
102         });
103
104         // Chunk through all chapters
105         $this->chapter->chunk(500, function ($chapters) {
106             $this->indexEntities($chapters);
107         });
108
109         // Chunk through all pages
110         $this->page->chunk(500, function ($pages) {
111             $this->indexEntities($pages);
112         });
113     }
114
115     /**
116      * Delete related Entity search terms.
117      * @param Entity $entity
118      */
119     public function deleteEntityTerms(Entity $entity)
120     {
121         $entity->searchTerms()->delete();
122     }
123
124     /**
125      * Create a scored term array from the given text.
126      * @param $text
127      * @param float|int $scoreAdjustment
128      * @return array
129      */
130     protected function generateTermArrayFromText($text, $scoreAdjustment = 1)
131     {
132         $tokenMap = []; // {TextToken => OccurrenceCount}
133         $splitText = explode(' ', $text);
134         foreach ($splitText as $token) {
135             if ($token === '') continue;
136             if (!isset($tokenMap[$token])) $tokenMap[$token] = 0;
137             $tokenMap[$token]++;
138         }
139
140         $terms = [];
141         foreach ($tokenMap as $token => $count) {
142             $terms[] = [
143                 'term' => $token,
144                 'score' => $count * $scoreAdjustment
145             ];
146         }
147         return $terms;
148     }
149
150 }