From 99587a0be63556a6915ac2728d8236da2f61c288 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 12 Nov 2021 17:06:01 +0000 Subject: [PATCH] Added tag values as part of the indexed search terms This allows finding content via tag name/values when just searching using normal seach terms. Added testing to cover. Related to #1577 --- app/Actions/Tag.php | 6 ++++++ app/Entities/Models/Book.php | 2 +- app/Entities/Models/Bookshelf.php | 2 +- app/Entities/Models/Chapter.php | 2 +- app/Entities/Tools/SearchIndex.php | 29 ++++++++++++++++++++++++++++- tests/Entity/EntitySearchTest.php | 19 +++++++++++++++---- 6 files changed, 52 insertions(+), 8 deletions(-) diff --git a/app/Actions/Tag.php b/app/Actions/Tag.php index db9328b7d..609c299ad 100644 --- a/app/Actions/Tag.php +++ b/app/Actions/Tag.php @@ -6,6 +6,12 @@ use BookStack\Model; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\MorphTo; +/** + * @property int $id + * @property string $name + * @property string $value + * @property int $order + */ class Tag extends Model { use HasFactory; diff --git a/app/Entities/Models/Book.php b/app/Entities/Models/Book.php index 359f7961c..735d25a99 100644 --- a/app/Entities/Models/Book.php +++ b/app/Entities/Models/Book.php @@ -24,7 +24,7 @@ class Book extends Entity implements HasCoverImage { use HasFactory; - public $searchFactor = 1.5; + public $searchFactor = 1.2; protected $fillable = ['name', 'description']; protected $hidden = ['restricted', 'pivot', 'image_id', 'deleted_at']; diff --git a/app/Entities/Models/Bookshelf.php b/app/Entities/Models/Bookshelf.php index b426858c3..e4d9775b7 100644 --- a/app/Entities/Models/Bookshelf.php +++ b/app/Entities/Models/Bookshelf.php @@ -13,7 +13,7 @@ class Bookshelf extends Entity implements HasCoverImage protected $table = 'bookshelves'; - public $searchFactor = 1.5; + public $searchFactor = 1.2; protected $fillable = ['name', 'description', 'image_id']; diff --git a/app/Entities/Models/Chapter.php b/app/Entities/Models/Chapter.php index f4d1a281d..224ded935 100644 --- a/app/Entities/Models/Chapter.php +++ b/app/Entities/Models/Chapter.php @@ -16,7 +16,7 @@ class Chapter extends BookChild { use HasFactory; - public $searchFactor = 1.5; + public $searchFactor = 1.2; protected $fillable = ['name', 'description', 'priority', 'book_id']; protected $hidden = ['restricted', 'pivot', 'deleted_at']; diff --git a/app/Entities/Tools/SearchIndex.php b/app/Entities/Tools/SearchIndex.php index bde5ef860..05de341f9 100644 --- a/app/Entities/Tools/SearchIndex.php +++ b/app/Entities/Tools/SearchIndex.php @@ -2,6 +2,7 @@ namespace BookStack\Entities\Tools; +use BookStack\Actions\Tag; use BookStack\Entities\EntityProvider; use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Page; @@ -84,6 +85,7 @@ class SearchIndex $entityModel->newQuery() ->select($selectFields) + ->with(['tags:id,name,value,entity_id,entity_type']) ->chunk($chunkSize, $chunkCallback); } } @@ -154,6 +156,30 @@ class SearchIndex return $scoresByTerm; } + /** + * Create a scored term map from the given set of entity tags. + * + * @param Tag[] $tags + * + * @returns array + */ + protected function generateTermScoreMapFromTags(array $tags): array + { + $scoreMap = []; + $names = []; + $values = []; + + foreach($tags as $tag) { + $names[] = $tag->name; + $values[] = $tag->value; + } + + $nameMap = $this->generateTermScoreMapFromText(implode(' ', $names), 3); + $valueMap = $this->generateTermScoreMapFromText(implode(' ', $values), 5); + + return $this->mergeTermScoreMaps($nameMap, $valueMap); + } + /** * For the given text, return an array where the keys are the unique term words * and the values are the frequency of that term. @@ -186,6 +212,7 @@ class SearchIndex protected function entityToTermDataArray(Entity $entity): array { $nameTermsMap = $this->generateTermScoreMapFromText($entity->name, 40 * $entity->searchFactor); + $tagTermsMap = $this->generateTermScoreMapFromTags($entity->tags->all()); if ($entity instanceof Page) { $bodyTermsMap = $this->generateTermScoreMapFromHtml($entity->html); @@ -193,7 +220,7 @@ class SearchIndex $bodyTermsMap = $this->generateTermScoreMapFromText($entity->description, $entity->searchFactor); } - $mergedScoreMap = $this->mergeTermScoreMaps($nameTermsMap, $bodyTermsMap); + $mergedScoreMap = $this->mergeTermScoreMaps($nameTermsMap, $bodyTermsMap, $tagTermsMap); $dataArray = []; $entityId = $entity->id; diff --git a/tests/Entity/EntitySearchTest.php b/tests/Entity/EntitySearchTest.php index bd50a13ac..08fabba0c 100644 --- a/tests/Entity/EntitySearchTest.php +++ b/tests/Entity/EntitySearchTest.php @@ -334,8 +334,7 @@ class EntitySearchTest extends TestCase
TermG
']); - $entityRelationCols = ['entity_id' => $page->id, 'entity_type' => 'BookStack\\Page']; - $scoreByTerm = SearchTerm::query()->where($entityRelationCols)->pluck('score', 'term'); + $scoreByTerm = $page->searchTerms()->pluck('score', 'term'); $this->assertEquals(1, $scoreByTerm->get('TermA')); $this->assertEquals(10, $scoreByTerm->get('TermB')); @@ -354,10 +353,22 @@ class EntitySearchTest extends TestCase

TermA

']); - $entityRelationCols = ['entity_id' => $page->id, 'entity_type' => 'BookStack\\Page']; - $scoreByTerm = SearchTerm::query()->where($entityRelationCols)->pluck('score', 'term'); + $scoreByTerm = $page->searchTerms()->pluck('score', 'term'); // Scores 40 for being in the name then 1 for being in the content $this->assertEquals(41, $scoreByTerm->get('TermA')); } + + public function test_tag_names_and_values_are_indexed_for_search() + { + $page = $this->newPage(['name' => 'PageA', 'html' => '

content

', 'tags' => [ + ['name' => 'Animal', 'value' => 'MeowieCat'], + ['name' => 'SuperImportant'], + ]]); + + $scoreByTerm = $page->searchTerms()->pluck('score', 'term'); + $this->assertEquals(5, $scoreByTerm->get('MeowieCat')); + $this->assertEquals(3, $scoreByTerm->get('Animal')); + $this->assertEquals(3, $scoreByTerm->get('SuperImportant')); + } }