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
This commit is contained in:
Dan Brown 2021-11-12 17:06:01 +00:00
parent f28daa01d9
commit 99587a0be6
No known key found for this signature in database
GPG key ID: 46D9F943C24A2EF9
6 changed files with 52 additions and 8 deletions

View file

@ -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;

View file

@ -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'];

View file

@ -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'];

View file

@ -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'];

View file

@ -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<string, int>
*/
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;

View file

@ -334,8 +334,7 @@ class EntitySearchTest extends TestCase
<h6>TermG</h6>
']);
$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
<p>TermA</p>
']);
$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' => '<p>content</p>', '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'));
}
}