3 namespace BookStack\Entities\Tools;
5 use BookStack\Entities\EntityProvider;
6 use BookStack\Entities\Models\Entity;
7 use BookStack\Entities\Models\SearchTerm;
8 use Illuminate\Support\Collection;
16 protected $entityProvider;
18 public function __construct(EntityProvider $entityProvider)
20 $this->entityProvider = $entityProvider;
24 * Index the given entity.
26 public function indexEntity(Entity $entity)
28 $this->deleteEntityTerms($entity);
29 $terms = $this->entityToTermDataArray($entity);
30 SearchTerm::query()->insert($terms);
34 * Index multiple Entities at once.
36 * @param Entity[] $entities
38 public function indexEntities(array $entities)
41 foreach ($entities as $entity) {
42 $entityTerms = $this->entityToTermDataArray($entity);
43 array_push($terms, ...$entityTerms);
46 $chunkedTerms = array_chunk($terms, 500);
47 foreach ($chunkedTerms as $termChunk) {
48 SearchTerm::query()->insert($termChunk);
53 * Delete and re-index the terms for all entities in the system.
54 * Can take a callback which is used for reporting progress.
55 * Callback receives three arguments:
56 * - An instance of the model being processed
57 * - The number that have been processed so far.
58 * - The total number of that model to be processed.
60 * @param callable(Entity, int, int)|null $progressCallback
62 public function indexAllEntities(?callable $progressCallback = null)
64 SearchTerm::query()->truncate();
66 foreach ($this->entityProvider->all() as $entityModel) {
67 $selectFields = ['id', 'name', $entityModel->textField];
68 $total = $entityModel->newQuery()->withTrashed()->count();
72 $chunkCallback = function (Collection $entities) use ($progressCallback, &$processed, $total, $chunkSize, $entityModel) {
73 $this->indexEntities($entities->all());
74 $processed = min($processed + $chunkSize, $total);
76 if (is_callable($progressCallback)) {
77 $progressCallback($entityModel, $processed, $total);
81 $entityModel->newQuery()
82 ->select($selectFields)
83 ->chunk($chunkSize, $chunkCallback);
88 * Delete related Entity search terms.
90 public function deleteEntityTerms(Entity $entity)
92 $entity->searchTerms()->delete();
96 * Create a scored term array from the given text.
98 * @returns array{term: string, score: float}
100 protected function generateTermArrayFromText(string $text, int $scoreAdjustment = 1): array
102 $tokenMap = []; // {TextToken => OccurrenceCount}
103 $splitChars = " \n\t.,!?:;()[]{}<>`'\"";
104 $token = strtok($text, $splitChars);
106 while ($token !== false) {
107 if (!isset($tokenMap[$token])) {
108 $tokenMap[$token] = 0;
111 $token = strtok($splitChars);
115 foreach ($tokenMap as $token => $count) {
118 'score' => $count * $scoreAdjustment,
126 * For the given entity, Generate an array of term data details.
127 * Is the raw term data, not instances of SearchTerm models.
129 * @returns array{term: string, score: float}[]
131 protected function entityToTermDataArray(Entity $entity): array
133 $nameTerms = $this->generateTermArrayFromText($entity->name, 40 * $entity->searchFactor);
134 $bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1 * $entity->searchFactor);
135 $termData = array_merge($nameTerms, $bodyTerms);
137 foreach ($termData as $index => $term) {
138 $termData[$index]['entity_type'] = $entity->getMorphClass();
139 $termData[$index]['entity_id'] = $entity->id;