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:
parent
f28daa01d9
commit
99587a0be6
|
@ -6,6 +6,12 @@ use BookStack\Model;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $id
|
||||||
|
* @property string $name
|
||||||
|
* @property string $value
|
||||||
|
* @property int $order
|
||||||
|
*/
|
||||||
class Tag extends Model
|
class Tag extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
|
@ -24,7 +24,7 @@ class Book extends Entity implements HasCoverImage
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
public $searchFactor = 1.5;
|
public $searchFactor = 1.2;
|
||||||
|
|
||||||
protected $fillable = ['name', 'description'];
|
protected $fillable = ['name', 'description'];
|
||||||
protected $hidden = ['restricted', 'pivot', 'image_id', 'deleted_at'];
|
protected $hidden = ['restricted', 'pivot', 'image_id', 'deleted_at'];
|
||||||
|
|
|
@ -13,7 +13,7 @@ class Bookshelf extends Entity implements HasCoverImage
|
||||||
|
|
||||||
protected $table = 'bookshelves';
|
protected $table = 'bookshelves';
|
||||||
|
|
||||||
public $searchFactor = 1.5;
|
public $searchFactor = 1.2;
|
||||||
|
|
||||||
protected $fillable = ['name', 'description', 'image_id'];
|
protected $fillable = ['name', 'description', 'image_id'];
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ class Chapter extends BookChild
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
public $searchFactor = 1.5;
|
public $searchFactor = 1.2;
|
||||||
|
|
||||||
protected $fillable = ['name', 'description', 'priority', 'book_id'];
|
protected $fillable = ['name', 'description', 'priority', 'book_id'];
|
||||||
protected $hidden = ['restricted', 'pivot', 'deleted_at'];
|
protected $hidden = ['restricted', 'pivot', 'deleted_at'];
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace BookStack\Entities\Tools;
|
namespace BookStack\Entities\Tools;
|
||||||
|
|
||||||
|
use BookStack\Actions\Tag;
|
||||||
use BookStack\Entities\EntityProvider;
|
use BookStack\Entities\EntityProvider;
|
||||||
use BookStack\Entities\Models\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
use BookStack\Entities\Models\Page;
|
use BookStack\Entities\Models\Page;
|
||||||
|
@ -84,6 +85,7 @@ class SearchIndex
|
||||||
|
|
||||||
$entityModel->newQuery()
|
$entityModel->newQuery()
|
||||||
->select($selectFields)
|
->select($selectFields)
|
||||||
|
->with(['tags:id,name,value,entity_id,entity_type'])
|
||||||
->chunk($chunkSize, $chunkCallback);
|
->chunk($chunkSize, $chunkCallback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -154,6 +156,30 @@ class SearchIndex
|
||||||
return $scoresByTerm;
|
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
|
* For the given text, return an array where the keys are the unique term words
|
||||||
* and the values are the frequency of that term.
|
* and the values are the frequency of that term.
|
||||||
|
@ -186,6 +212,7 @@ class SearchIndex
|
||||||
protected function entityToTermDataArray(Entity $entity): array
|
protected function entityToTermDataArray(Entity $entity): array
|
||||||
{
|
{
|
||||||
$nameTermsMap = $this->generateTermScoreMapFromText($entity->name, 40 * $entity->searchFactor);
|
$nameTermsMap = $this->generateTermScoreMapFromText($entity->name, 40 * $entity->searchFactor);
|
||||||
|
$tagTermsMap = $this->generateTermScoreMapFromTags($entity->tags->all());
|
||||||
|
|
||||||
if ($entity instanceof Page) {
|
if ($entity instanceof Page) {
|
||||||
$bodyTermsMap = $this->generateTermScoreMapFromHtml($entity->html);
|
$bodyTermsMap = $this->generateTermScoreMapFromHtml($entity->html);
|
||||||
|
@ -193,7 +220,7 @@ class SearchIndex
|
||||||
$bodyTermsMap = $this->generateTermScoreMapFromText($entity->description, $entity->searchFactor);
|
$bodyTermsMap = $this->generateTermScoreMapFromText($entity->description, $entity->searchFactor);
|
||||||
}
|
}
|
||||||
|
|
||||||
$mergedScoreMap = $this->mergeTermScoreMaps($nameTermsMap, $bodyTermsMap);
|
$mergedScoreMap = $this->mergeTermScoreMaps($nameTermsMap, $bodyTermsMap, $tagTermsMap);
|
||||||
|
|
||||||
$dataArray = [];
|
$dataArray = [];
|
||||||
$entityId = $entity->id;
|
$entityId = $entity->id;
|
||||||
|
|
|
@ -334,8 +334,7 @@ class EntitySearchTest extends TestCase
|
||||||
<h6>TermG</h6>
|
<h6>TermG</h6>
|
||||||
']);
|
']);
|
||||||
|
|
||||||
$entityRelationCols = ['entity_id' => $page->id, 'entity_type' => 'BookStack\\Page'];
|
$scoreByTerm = $page->searchTerms()->pluck('score', 'term');
|
||||||
$scoreByTerm = SearchTerm::query()->where($entityRelationCols)->pluck('score', 'term');
|
|
||||||
|
|
||||||
$this->assertEquals(1, $scoreByTerm->get('TermA'));
|
$this->assertEquals(1, $scoreByTerm->get('TermA'));
|
||||||
$this->assertEquals(10, $scoreByTerm->get('TermB'));
|
$this->assertEquals(10, $scoreByTerm->get('TermB'));
|
||||||
|
@ -354,10 +353,22 @@ class EntitySearchTest extends TestCase
|
||||||
<p>TermA</p>
|
<p>TermA</p>
|
||||||
']);
|
']);
|
||||||
|
|
||||||
$entityRelationCols = ['entity_id' => $page->id, 'entity_type' => 'BookStack\\Page'];
|
$scoreByTerm = $page->searchTerms()->pluck('score', 'term');
|
||||||
$scoreByTerm = SearchTerm::query()->where($entityRelationCols)->pluck('score', 'term');
|
|
||||||
|
|
||||||
// Scores 40 for being in the name then 1 for being in the content
|
// Scores 40 for being in the name then 1 for being in the content
|
||||||
$this->assertEquals(41, $scoreByTerm->get('TermA'));
|
$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'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue