]> BookStack Code Mirror - bookstack/blob - app/Entities/Tools/SearchIndex.php
16f261a37b1aa263f89f37ae747ff79cb305cdd3
[bookstack] / app / Entities / Tools / SearchIndex.php
1 <?php
2
3 namespace BookStack\Entities\Tools;
4
5 use BookStack\Entities\EntityProvider;
6 use BookStack\Entities\Models\Entity;
7 use BookStack\Entities\Models\SearchTerm;
8 use Illuminate\Support\Collection;
9
10 class SearchIndex
11 {
12
13     /**
14      * @var EntityProvider
15      */
16     protected $entityProvider;
17
18     public function __construct(EntityProvider $entityProvider)
19     {
20         $this->entityProvider = $entityProvider;
21     }
22
23     /**
24      * Index the given entity.
25      */
26     public function indexEntity(Entity $entity)
27     {
28         $this->deleteEntityTerms($entity);
29         $terms = $this->entityToTermDataArray($entity);
30         SearchTerm::query()->insert($terms);
31     }
32
33     /**
34      * Index multiple Entities at once.
35      *
36      * @param Entity[] $entities
37      */
38     public function indexEntities(array $entities)
39     {
40         $terms = [];
41         foreach ($entities as $entity) {
42             $entityTerms = $this->entityToTermDataArray($entity);
43             array_push($terms, ...$entityTerms);
44         }
45
46         $chunkedTerms = array_chunk($terms, 500);
47         foreach ($chunkedTerms as $termChunk) {
48             SearchTerm::query()->insert($termChunk);
49         }
50     }
51
52     /**
53      * Delete and re-index the terms for all entities in the system.
54      */
55     public function indexAllEntities()
56     {
57         SearchTerm::query()->truncate();
58
59         foreach ($this->entityProvider->all() as $entityModel) {
60             $selectFields = ['id', 'name', $entityModel->textField];
61             $entityModel->newQuery()
62                 ->withTrashed()
63                 ->select($selectFields)
64                 ->chunk(1000, function (Collection $entities) {
65                     $this->indexEntities($entities->all());
66                 });
67         }
68     }
69
70     /**
71      * Delete related Entity search terms.
72      */
73     public function deleteEntityTerms(Entity $entity)
74     {
75         $entity->searchTerms()->delete();
76     }
77
78     /**
79      * Create a scored term array from the given text.
80      *
81      * @returns array{term: string, score: float}
82      */
83     protected function generateTermArrayFromText(string $text, int $scoreAdjustment = 1): array
84     {
85         $tokenMap = []; // {TextToken => OccurrenceCount}
86         $splitChars = " \n\t.,!?:;()[]{}<>`'\"";
87         $token = strtok($text, $splitChars);
88
89         while ($token !== false) {
90             if (!isset($tokenMap[$token])) {
91                 $tokenMap[$token] = 0;
92             }
93             $tokenMap[$token]++;
94             $token = strtok($splitChars);
95         }
96
97         $terms = [];
98         foreach ($tokenMap as $token => $count) {
99             $terms[] = [
100                 'term'  => $token,
101                 'score' => $count * $scoreAdjustment,
102             ];
103         }
104
105         return $terms;
106     }
107
108     /**
109      * For the given entity, Generate an array of term data details.
110      * Is the raw term data, not instances of SearchTerm models.
111      *
112      * @returns array{term: string, score: float}[]
113      */
114     protected function entityToTermDataArray(Entity $entity): array
115     {
116         $nameTerms = $this->generateTermArrayFromText($entity->name, 40 * $entity->searchFactor);
117         $bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1 * $entity->searchFactor);
118         $termData = array_merge($nameTerms, $bodyTerms);
119
120         foreach ($termData as $index => $term) {
121             $termData[$index]['entity_type'] = $entity->getMorphClass();
122             $termData[$index]['entity_id'] = $entity->id;
123         }
124
125         return $termData;
126     }
127 }