Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Content Search: use AND operator by default and don't apply mask for phrases #6979

Merged
merged 24 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0b2ce57
Content Search: use AND operator by default and don't apply mask for …
yurabakhtin Apr 30, 2024
fcbe691
Fix test
yurabakhtin Apr 30, 2024
4f92d84
Fix SearchQueryTest
yurabakhtin Apr 30, 2024
a74a574
Merge branch 'develop' into enh/230-content-search-default-operator
yurabakhtin Apr 30, 2024
c233527
Merge branch 'develop' into enh/230-content-search-default-operator
yurabakhtin May 2, 2024
06c6fe0
Fix creating of FULLTEXT index for mysql 8+
yurabakhtin May 2, 2024
c75eee8
Merge branch 'develop' into enh/230-content-search-default-operator
yurabakhtin May 2, 2024
92e4881
Fix mysql search driver for short words
yurabakhtin May 3, 2024
4fc9d98
Improve keyword highlighting
yurabakhtin May 3, 2024
30379f1
Use fixed value instead of `mysql.ft_min_word_len`
yurabakhtin May 3, 2024
ce85d0c
Exclude highlighting on console request
yurabakhtin May 6, 2024
308e400
Escape special char `@` on mysql search driver
yurabakhtin May 6, 2024
ff2705f
Merge branch 'develop' into enh/230-content-search-default-operator
yurabakhtin May 7, 2024
7aed7d6
Escape special chars on mysql search driver
yurabakhtin May 7, 2024
3673239
Escape char `/` on mysql search driver
yurabakhtin May 7, 2024
b37078d
Escape quote chars on mysql search driver
yurabakhtin May 7, 2024
bda6c61
Improve word highlighting with quote or apostrophe
yurabakhtin May 7, 2024
a6bcf94
Merge branch 'develop' into enh/230-content-search-default-operator
yurabakhtin May 9, 2024
9cec0c6
Add code comment for search query and driver
yurabakhtin May 9, 2024
9faa03a
Merge branch 'develop' into enh/230-content-search-default-operator
yurabakhtin May 16, 2024
b850754
Refactor search query
yurabakhtin May 17, 2024
5bdb059
Fix test for ZendLucence Driver
yurabakhtin May 17, 2024
2d165ae
Fix ZendLucence Driver for phrases
yurabakhtin May 17, 2024
f21605e
Merge branch 'develop' into enh/230-content-search-default-operator
luke- May 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG-DEV.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ HumHub Changelog
- Enh #6984: In forms, change checkbox style to match other input types
- Enh #6986: When moving a content from a container to another, prevent updating the content dates to keep the stream sort as it was
- Enh #6992: Improve handle database connection errors
- Enh #6979: Content Search: use AND operator by default and don't apply mask for phrases

1.16.0-beta.2 (April 9, 2024)
-----------------------------
Expand Down
28 changes: 18 additions & 10 deletions protected/humhub/libs/SearchQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,20 @@ public function __construct(string $query)

if (!empty($result[0]) && is_array($result[0])) {
foreach ($result[0] as $i => $term) {
if (!preg_match('/^(\+|\-|AND |NOT )?".+"$/', $term)) {
// A not quoted term should be searched with mask by default
$term = rtrim($term, '*"') . '*';
}

// Remove quotation marks
$term = str_replace('"', '', $term);

if (str_starts_with($term, '+') || str_starts_with($term, 'AND ')) {
if (str_starts_with($term, 'OR ')) {
$orTerms[] = preg_replace('/^((?i)OR )?/', '', $term);
} elseif (str_starts_with($term, '-') || str_starts_with($term, 'NOT ')) {
$notTerms[] = preg_replace('/^\-*((?i)NOT )?/', '', $term);
} else {
// Use AND operator by default

/**
* Special Case: In search queries like "Apple AND Banana", Apple should
Expand All @@ -75,19 +84,18 @@ public function __construct(string $query)
$orTerms = [];
}

$andTerms[] = preg_replace('/^\+?((?i)AND )?/', '', $term);
} elseif (str_starts_with($term, '-') || str_starts_with($term, 'NOT ')) {
$notTerms[] = preg_replace('/^\-?((?i)NOT )?/', '', $term);
} else {
$orTerms[] = preg_replace('/^((?i)OR )?/', '', $term);
$andTerms[] = preg_replace('/^\+*((?i)AND )?/', '', $term);
}
}
}

$this->notTerms = array_filter($notTerms);
$this->orTerms = array_filter($orTerms);
$this->andTerms = array_filter($andTerms);
$this->notTerms = array_filter($notTerms, [$this, 'filterEmptyTerms']);
$this->orTerms = array_filter($orTerms, [$this, 'filterEmptyTerms']);
$this->andTerms = array_filter($andTerms, [$this, 'filterEmptyTerms']);
}


private function filterEmptyTerms($term): bool
{
return !empty($term) && $term !== '*';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,8 @@ humhub.module('comment', function (module, require, $) {

// Highlight currently searching keywords in the loaded comments
const contentSearchKeyword = $('.container-contents .form-search input[name=keyword]');
if (contentSearchKeyword.length && contentSearchKeyword.val().length) {
contentSearchKeyword.val().split(' ').forEach((keyword) => $html.highlight(keyword))
if (contentSearchKeyword.length) {
additions.highlightWords($html, contentSearchKeyword.val());
}
}).catch(function (err) {
module.log.error(err, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ class ContentHighlightAsset extends AssetBundle
*/
public static function register($view)
{
$highlight = Yii::$app->session->get('contentHighlight');
if ($highlight !== null && $highlight !== '') {
Yii::$app->session->remove('contentHighlight');
$view->registerJsConfig('content.highlight', ['keyword' => $highlight]);
if (!Yii::$app->request->isConsoleRequest) {
$highlight = Yii::$app->session->get('contentHighlight');
if ($highlight !== null && $highlight !== '') {
Yii::$app->session->remove('contentHighlight');
$view->registerJsConfig('content.highlight', ['keyword' => $highlight]);
}
}

return parent::register($view);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public function safeUp()
$this->safeAddForeignKey('fk_content_fulltext', 'content_fulltext', 'content_id', 'content', 'id', 'CASCADE', 'CASCADE');

try {
$this->execute("ALTER TABLE content_fulltext ADD FULLTEXT INDEX ftx (contents ASC, comments ASC, files ASC)");
$this->execute('ALTER TABLE content_fulltext ADD FULLTEXT INDEX ftx (contents, comments, files)');

Yii::$app->queue->push(new SearchRebuildIndex());

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
humhub.module('content.highlight', function (module, require, $) {
const Widget = require('ui.widget').Widget;
const event = require('event');
const highlightWords = require('ui.additions').highlightWords;

const layout = $('.layout-content-container');

const init = function () {
event.on('humhub:modules:content:highlight:afterInit', () => highlight());
layout.find('[data-ui-widget="ui.richtext.prosemirror.RichText"]')
.on('afterRender', (obj) => highlight($(obj.target)));
.on('afterRender', (obj) => highlight(obj.target));

const wallStream = Widget.instance('[data-ui-widget="stream.wall.WallStream"]');
if (wallStream) {
Expand All @@ -20,7 +21,7 @@ humhub.module('content.highlight', function (module, require, $) {
if (typeof object === 'undefined') {
object = layout;
}
module.config.keyword.split(' ').forEach((keyword) => object.highlight(keyword));
highlightWords(object, module.config.keyword);
}
}

Expand Down
24 changes: 21 additions & 3 deletions protected/humhub/modules/content/search/driver/MysqlDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@

class MysqlDriver extends AbstractDriver
{
/**
* Minimum word length for "And Terms",
* Words with less length are handled as "Or Terms"
* NOTE: Using of the config value mysql.ft_min_word_len doesn't work properly.
*
* @var int $minAndTermLength
*/
public int $minAndTermLength = 3;

public function purge(): void
{
ContentFulltext::deleteAll();
Expand Down Expand Up @@ -129,13 +138,17 @@ private function createMysqlFullTextQuery(SearchQuery $query, array $matchFields
$againstQuery = '';

foreach ($query->andTerms as $keyword) {
$againstQuery .= '+' . rtrim($keyword, '*') . '* ';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yurabakhtin Is there a reason why you removed the wildcards?

Basically, the search for “Apple” should also find “Applepie”.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@luke- I have moved the wildcards from drivers(mysql, zend, solr) to SearchQuery code here, because the chars " are removed on the SearchQuery side, so we cannot know what original term was really when we are on the driver side.

This is how it worked before the change:

  • Apple => Apple* (find Applepie)
  • "Apple" => Apple* (find Applepie)

After the change:

  • Apple => Apple* (find Applepie)
  • "Apple" => Apple (find only Apple, no Applepie)

As I understand it should work as it is described in the first table here.

Tests for quoted cases are here:

I have done the change for the request "Phrases should actually work. Can you check what is causing the problem here and add tests if necessary?", please correct me if I misunderstood it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yurabakhtin My intention was for “SearchQuery” to be as neutral a representation of the search query as possible. Without any syntax like wildcards or terms in quotation marks.

e.g. Asterisk or Quotation marks is more of a driver-specific syntax. (Even if the similar for MySQL and Lucene)

But I understand that, for example, with SearchRequest::andTerms we cannot distinguish whether it was originally quoted or not.

Do you have any ideas?

  • We could introduce new attributes such as andParts (with Wildcard) and andTerms (with Quotationmarks).

  • We could define that as soon as a space is contained in e.g. andTerms, it is seen as a term, otherwise it is provided with a wildcard. (But then “Apple” always becomes Apple*.

  • ?

Or did I get something wrong here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@luke-

My intention was for “SearchQuery” to be as neutral a representation of the search query as possible. Without any syntax like wildcards or terms in quotation marks.

e.g. Asterisk or Quotation marks is more of a driver-specific syntax. (Even if the similar for MySQL and Lucene)

Ok, I have understood your intention.

For solve it we could implement new array as andParts or extend the existing array with new array item to mark each term like this:

SearchQuery->andTerms = [
    ['Apple', 'mask' => false],
    ['Pine', 'mask' => true],
];

so on driver side we will know what term should be masked with *.

But I am thinking why should we keep an additional array andParts or use the complex structure if we can use a last char of each term as flag. I.e. if the last char is * then we can decide such term should be processed with specific way on a driver side.
For current drivers(mysql, zend, solr) we can keep the code as is, but for future drivers the last char * can be used as flag like this, if the driver doesn't support terms ended with *:

foreach ($request->getSearchQuery()->andTerms as $term) {
    $keywordQuery->addSubquery(str_ends_with($term, '*') ? $this->prepareMaskedTerm($term) : $term);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yurabakhtin Okay, I got your point.

Do I understand it correctly that now e.g. in SearchQuery::andTerms, can contain entries that are either quoted and contain an asterisk. Or is only the asterisk included?

Your approach is probably the right one. Since in fact all current drivers support this and it also corresponds to the query language of SearchQuery.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I understand it correctly that now e.g. in SearchQuery::andTerms, can contain entries that are either quoted and contain an asterisk. Or is only the asterisk included?

@luke- Yes, but a bit correction about "quoted":

  • entries that are either simple word without quotes or contain an asterisk

Examples how a requested keyword is stored in terms:

  • Apple => Apple*
  • "Apple" => Apple
  • Foo bar => Foo bar*
  • "Foo bar" => Foo bar

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yurabakhtin Can you please add this example somewhere in the SearchQuery PHPDoc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yurabakhtin Can you please add this example somewhere in the SearchQuery PHPDoc?

@luke- 9cec0c6#diff-70f82fb3d09d4959fb9f44a526f8455144c112df88ec7299a2efe28a76171169R28-R64

if (strlen(rtrim($keyword, '*')) < $this->minAndTermLength) {
$againstQuery .= $this->prepareKeyword($keyword) . ' ';
} else {
$againstQuery .= '+' . $this->prepareKeyword($keyword) . ' ';
}
}
foreach ($query->orTerms as $keyword) {
$againstQuery .= rtrim($keyword, '*') . '* ';
$againstQuery .= $this->prepareKeyword($keyword) . ' ';
}
foreach ($query->notTerms as $keyword) {
$againstQuery .= '-' . $keyword . ' ';
$againstQuery .= '-' . $this->prepareKeyword($keyword) . ' ';
}

return sprintf(
Expand All @@ -145,6 +158,11 @@ private function createMysqlFullTextQuery(SearchQuery $query, array $matchFields
);
}

protected function prepareKeyword(string $keyword): string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I understand correctly?

Automatically enclose keywords with special characters in quotation marks.

If yes, can you please create a small comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@luke- Yes, correct, I have added a comment here.

{
return preg_match('#[\s@<>~%\(\)\$/’\']#', $keyword) ? '"' . $keyword . '"' : $keyword;
}

protected function addQueryFilterVisibility(ActiveQuery $query): ActiveQuery
{
$query->andWhere(['content.state' => Content::STATE_PUBLISHED]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,11 @@ protected function buildSearchQuery(SearchRequest $request): Boolean

$keywordQuery = new Boolean();
foreach ($request->getSearchQuery()->orTerms as $term) {
$keywordQuery->addSubquery(new Wildcard(new Term(mb_strtolower($term) . '*')), null);
$keywordQuery->addSubquery(new Wildcard(new Term(mb_strtolower($term))), null);
}

foreach ($request->getSearchQuery()->andTerms as $term) {
$keywordQuery->addSubquery(new Wildcard(new Term(mb_strtolower($term) . '*')), true);
$keywordQuery->addSubquery(new Wildcard(new Term(mb_strtolower($term))), true);
}

foreach ($request->getSearchQuery()->notTerms as $term) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,17 @@ public function testKeywords()
// Test Multiple AND Keywords
#$this->assertCount(1, $this->getSearchResultByKeyword('Marabru')->results);

$this->assertCount(1, $this->getSearchResultByKeyword('Marabru Leav Abcd')->results);
$this->assertCount(1, $this->getSearchResultByKeyword('"Marabru" Tes')->results);
$this->assertCount(0, $this->getSearchResultByKeyword('"Marabr" Test')->results);

$this->assertCount(1, $this->getSearchResultByKeyword('Marabru Leav')->results);
$this->assertCount(1, $this->getSearchResultByKeyword('Marabru Leav OR Abcd')->results);
$this->assertCount(2, $this->getSearchResultByKeyword('OR Marabru OR Something')->results);
$this->assertCount(0, $this->getSearchResultByKeyword('+Marabru +Leav* +Abcd')->results);
$this->assertCount(0, $this->getSearchResultByKeyword('Marabru Leav +Abcd')->results);

$this->assertCount(1, $this->getSearchResultByKeyword('Something -Marabru')->results);
$this->assertCount(1, $this->getSearchResultByKeyword('Something -Marab')->results);

// Wildcards
$this->assertCount(1, $this->getSearchResultByKeyword('Marabr*')->results);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ humhub.module('stream.SimpleStream', function (module, require, $) {
var content = require('content');
var Url = require('ui.filter').Url;
var Widget = require('ui.widget').Widget;
var highlightWords = require('ui.additions').highlightWords;

/**
* Simple stream component can be used for static streams without load logic (only reload single content).
Expand Down Expand Up @@ -58,7 +59,7 @@ humhub.module('stream.SimpleStream', function (module, require, $) {
widgets.on('afterInit', function() {
if (!$(this).data('isHighlighted')) {
$(this).data('isHighlighted', true);
that.highlightInput.val().split(' ').forEach((keyword) => $(this).highlight(keyword));
highlightWords(this, that.highlightInput.val());
}
});
}
Expand Down
57 changes: 31 additions & 26 deletions protected/humhub/tests/codeception/unit/libs/SearchQueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,64 +16,70 @@ public function testTermsWithSigns()
{
$query = new SearchQuery('Apple Pie +"Foo" -"Foo bar"');

$this->assertContains('Apple', $query->orTerms);
$this->assertContains('Pie', $query->orTerms);
$this->assertContains('Apple*', $query->andTerms);
$this->assertContains('Pie*', $query->andTerms);
$this->assertContains('Foo', $query->andTerms);
$this->assertContains('Foo bar', $query->notTerms);

$query = new SearchQuery('Apple');
$this->assertContains('Apple', $query->orTerms);
$this->assertContains('Apple*', $query->andTerms);
$this->assertEmpty($query->orTerms);
$this->assertEmpty($query->notTerms);
$this->assertEmpty($query->andTerms);

$query = new SearchQuery('-Apple');
$this->assertContains('Apple', $query->notTerms);
$this->assertContains('Apple*', $query->notTerms);
$this->assertEmpty($query->orTerms);
$this->assertEmpty($query->andTerms);

$query = new SearchQuery('Apple +Banana');
$this->assertContains('Apple', $query->orTerms);
$this->assertContains('Banana', $query->andTerms);
$this->assertContains('Apple*', $query->andTerms);
$this->assertContains('Banana*', $query->andTerms);

$query = new SearchQuery('----Apple +++++Banana "---Orange" "++++Peach"');
$this->assertContains('Apple*', $query->notTerms);
$this->assertContains('Banana*', $query->andTerms);
$this->assertContains('Orange', $query->notTerms);
$this->assertContains('Peach', $query->andTerms);
}

public function testTermsWithWords()
{
$query = new SearchQuery('Apple Pie AND "Foo" NOT "Foo bar"');

$this->assertContains('Apple', $query->orTerms);
$this->assertContains('Pie', $query->orTerms);
$this->assertContains('Apple*', $query->andTerms);
$this->assertContains('Pie*', $query->andTerms);
$this->assertContains('Foo', $query->andTerms);
$this->assertContains('Foo bar', $query->notTerms);

$query = new SearchQuery('Apple');
$this->assertContains('Apple', $query->orTerms);
$this->assertContains('Apple*', $query->andTerms);
$this->assertEmpty($query->orTerms);
$this->assertEmpty($query->notTerms);
$this->assertEmpty($query->andTerms);

$query = new SearchQuery('NOT Apple');
$this->assertContains('Apple', $query->notTerms);
$this->assertContains('Apple*', $query->notTerms);
$this->assertEmpty($query->orTerms);
$this->assertEmpty($query->andTerms);

$query = new SearchQuery('Apple OR Banana');
$this->assertContains('Apple', $query->orTerms);
$this->assertContains('Banana', $query->orTerms);
$this->assertContains('Apple*', $query->andTerms);
$this->assertContains('Banana*', $query->orTerms);

$query = new SearchQuery('Apple AND Banana');
$this->assertContains('Apple', $query->andTerms);
$this->assertContains('Banana', $query->andTerms);
$this->assertContains('Apple*', $query->andTerms);
$this->assertContains('Banana*', $query->andTerms);

$query = new SearchQuery('Apple AND Banana OR Grape');
$this->assertContains('Apple', $query->andTerms);
$this->assertContains('Banana', $query->andTerms);
$this->assertContains('Grape', $query->orTerms);
$this->assertContains('Apple*', $query->andTerms);
$this->assertContains('Banana*', $query->andTerms);
$this->assertContains('Grape*', $query->orTerms);
}

public function testInvalid()
{
$query = new SearchQuery('Apple "Pie');
$this->assertContains('Apple', $query->orTerms);
$this->assertContains('Pie', $query->orTerms);
$this->assertContains('Apple*', $query->andTerms);
$this->assertContains('Pie*', $query->andTerms);

$query = new SearchQuery('');
$this->assertEmpty($query->orTerms);
Expand All @@ -99,13 +105,12 @@ public function testInvalid()
public function testTermsWithNumbers()
{
$query = new SearchQuery('Quote 2024');
$this->assertContains('2024', $query->orTerms);
$this->assertContains('Quote', $query->orTerms);
$this->assertEmpty($query->andTerms);
$this->assertContains('2024*', $query->andTerms);
$this->assertContains('Quote*', $query->andTerms);
$this->assertEmpty($query->orTerms);
$this->assertEmpty($query->notTerms);

$query = new SearchQuery('"Quote 2024"');
$this->assertContains('Quote 2024', $query->orTerms);

$this->assertContains('Quote 2024', $query->andTerms);
}
}
24 changes: 23 additions & 1 deletion static/js/humhub/humhub.ui.additions.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,27 @@ humhub.module('ui.additions', function (module, require, $) {
});
};

var highlightWords = function (node, words) {
var $node = node instanceof $ ? node : $(node);
if (!$node.length || typeof($node.highlight) !== 'function') {
return;
}

if (typeof words === 'string' && words !== '') {
words = words.split(/[^\p{Script=Latin}\d\-’']+/u)
}
if (!Array.isArray(words)) {
return;
}

words.forEach(function (word) {
word = word.replace(/^([^a-z\d]*)(.+?)([^a-z\d]*)$/i, '$2');
$node.highlight(word);
word.indexOf("'") > -1 && $node.highlight(word.replace("'", '’'));
word.indexOf("’") > -1 && $node.highlight(word.replace('’', "'"));
});
};

var observe = function (node, options) {
if (object.isBoolean(options)) {
options = {applyOnInit: options};
Expand Down Expand Up @@ -338,7 +359,8 @@ humhub.module('ui.additions', function (module, require, $) {
extend: extend,
register: register,
switchButtons: switchButtons,
highlight: highlight
highlight: highlight,
highlightWords: highlightWords,
});
});

Expand Down
4 changes: 2 additions & 2 deletions static/js/humhub/humhub.ui.search.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ humhub.module('ui.search', function(module, require, $) {
const client = require('client');
const loader = require('ui.loader');
const Widget = require('ui.widget').Widget;
const highlightWords = require('ui.additions').highlightWords;

const Search = Widget.extend();

Expand Down Expand Up @@ -305,8 +306,7 @@ humhub.module('ui.search', function(module, require, $) {
provider.replaceWith(newProviderContent);
const records = newProviderContent.find(that.selectors.providerRecord);
if (records.length) {
const highlightedText = records.find(that.selectors.providerRecordText);
data.keyword.split(' ').forEach((keyword) => highlightedText.highlight(keyword));
highlightWords(records.find(that.selectors.providerRecordText), data.keyword);
} else if (newProviderContent.data('hide-on-empty') !== undefined) {
newProviderContent.hide();
}
Expand Down