full text search 2
This commit is contained in:
@@ -53,6 +53,8 @@ class DataManager
|
||||
*/
|
||||
public function rebuild($entityList = null)
|
||||
{
|
||||
$this->populateConfigParameters();
|
||||
|
||||
$result = $this->clearCache();
|
||||
|
||||
$result &= $this->rebuildMetadata();
|
||||
@@ -172,5 +174,29 @@ class DataManager
|
||||
$this->getContainer()->get('config')->save();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
protected function populateConfigParameters()
|
||||
{
|
||||
$config = $this->getContainer()->get('config');
|
||||
|
||||
$pdo = $this->getContainer()->get('entityManager')->getPDO();
|
||||
$query = "SHOW VARIABLES LIKE 'ft_min_word_len'";
|
||||
$sth = $pdo->prepare($query);
|
||||
$sth->execute();
|
||||
|
||||
$fullTextSearchMinLength = null;
|
||||
if ($row = $sth->fetch(\PDO::FETCH_ASSOC)) {
|
||||
if (isset($row['Value'])) {
|
||||
$fullTextSearchIsNotAvailable = false;
|
||||
$fullTextSearchMinLength = intval($row['Value']);
|
||||
}
|
||||
} else {
|
||||
$fullTextSearchIsNotAvailable = true;
|
||||
}
|
||||
|
||||
$config->set('fullTextSearchIsNotAvailable', $fullTextSearchIsNotAvailable);
|
||||
$config->set('fullTextSearchMinLength', $fullTextSearchMinLength);
|
||||
|
||||
$config->save();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,8 @@ class Base
|
||||
|
||||
const MIN_LENGTH_FOR_FULL_TEXT_SEARCH = 4;
|
||||
|
||||
protected $fullTextSearchDataCacheHash = [];
|
||||
|
||||
public function __construct($entityManager, \Espo\Entities\User $user, Acl $acl, AclManager $aclManager, Metadata $metadata, Config $config, InjectableFactory $injectableFactory)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
@@ -205,8 +207,8 @@ class Base
|
||||
}
|
||||
$this->applyBoolFilter($filter, $result);
|
||||
}
|
||||
} else if ($item['type'] == 'textFilter' && !empty($item['value'])) {
|
||||
if (!empty($item['value'])) {
|
||||
} else if ($item['type'] == 'textFilter') {
|
||||
if (isset($item['value']) || $item['value'] !== '') {
|
||||
$this->textFilter($item['value'], $result);
|
||||
}
|
||||
} else if ($item['type'] == 'primary' && !empty($item['value'])) {
|
||||
@@ -389,7 +391,7 @@ class Base
|
||||
|
||||
protected function q($params, &$result)
|
||||
{
|
||||
if (!empty($params['q'])) {
|
||||
if (isset($params['q']) && $params['q'] !== '') {
|
||||
$this->textFilter($params['q'], $result);
|
||||
}
|
||||
}
|
||||
@@ -403,7 +405,7 @@ class Base
|
||||
public function manageTextFilter($textFilter, &$result)
|
||||
{
|
||||
$this->prepareResult($result);
|
||||
$this->q(array('q' => $textFilter), $result);
|
||||
$this->q(['q' => $textFilter], $result);
|
||||
}
|
||||
|
||||
public function getEmptySelectParams()
|
||||
@@ -734,7 +736,7 @@ class Base
|
||||
$this->where($params['where'], $result);
|
||||
}
|
||||
|
||||
if (!empty($params['textFilter'])) {
|
||||
if (isset($params['textFilter']) && $params['textFilter'] !== '') {
|
||||
$this->textFilter($params['textFilter'], $result);
|
||||
}
|
||||
|
||||
@@ -1502,18 +1504,32 @@ class Base
|
||||
);
|
||||
}
|
||||
|
||||
public function getFullTextSearchDataForTextFilter($textFilter)
|
||||
public function getFullTextSearchDataForTextFilter($textFilter, $stripWildcard = false)
|
||||
{
|
||||
if (array_key_exists($textFilter, $this->fullTextSearchDataCacheHash)) {
|
||||
return $this->fullTextSearchDataCacheHash[$textFilter];
|
||||
}
|
||||
|
||||
if ($this->getConfig()->get('fullTextSearchIsNotAvailable')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$result = null;
|
||||
|
||||
$fieldList = $this->getTextFilterFieldList();
|
||||
|
||||
$useFullTextSearch = false;
|
||||
if ($this->getMetadata()->get(['scopes', $this->getEntityType(), 'fullTextSearch'])) {
|
||||
if (mb_strlen($textFilter) >= $this->getConfig()->get('fullTextSearchMinLength', self::MIN_LENGTH_FOR_FULL_TEXT_SEARCH)) {
|
||||
$useFullTextSearch = true;
|
||||
if ($stripWildcard) {
|
||||
$textFilter = str_replace('%', '', $textFilter);
|
||||
}
|
||||
|
||||
if (mb_strpos($textFilter, '%')) {
|
||||
$useFullTextSearch = false;
|
||||
}
|
||||
$useFullTextSearch = false;
|
||||
if ($this->getMetadata()->get(['entityDefs', $this->getEntityType(), 'collection', 'fullTextSearch'])) {
|
||||
$fullTextSearchMinLength = $this->getConfig()->get('fullTextSearchMinLength', self::MIN_LENGTH_FOR_FULL_TEXT_SEARCH);
|
||||
if (!$fullTextSearchMinLength) {
|
||||
$fullTextSearchMinLength = 0;
|
||||
}
|
||||
if (mb_strlen($textFilter) >= $fullTextSearchMinLength) {
|
||||
$useFullTextSearch = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1553,14 +1569,16 @@ class Base
|
||||
$fullTextSearchFieldSanitizedList[$i] = $this->getEntityManager()->getQuery()->sanitize($field);
|
||||
}
|
||||
|
||||
$where = $function . ':' . implode(',', $fullTextSearchFieldSanitizedList) . ':' . $this->getEntityManager()->getQuery()->quote($textFilter);
|
||||
return [
|
||||
$where = $function . ':' . implode(',', $fullTextSearchFieldSanitizedList) . ':' . $textFilter;
|
||||
$result = [
|
||||
'where' => $where,
|
||||
'fieldList' => $fullTextSearchFieldList
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
$this->fullTextSearchDataCacheHash[$textFilter] = $result;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function textFilter($textFilter, &$result)
|
||||
@@ -1571,40 +1589,91 @@ class Base
|
||||
|
||||
$textFilterContainsMinLength = $this->getConfig()->get('textFilterContainsMinLength', self::MIN_LENGTH_FOR_CONTENT_SEARCH);
|
||||
|
||||
$fullTextSearchData = $this->getFullTextSearchDataForTextFilter($textFilter);
|
||||
$fullTextSearchData = null;
|
||||
|
||||
$forceFullTextSearch = false;
|
||||
|
||||
$useFullTextSearch = !empty($result['forceFullTextSearch']);
|
||||
|
||||
if (mb_strpos($textFilter, 'ft:') === 0) {
|
||||
$textFilter = mb_substr($textFilter, 3);
|
||||
$useFullTextSearch = true;
|
||||
}
|
||||
|
||||
$skipWidlcards = false;
|
||||
if (!$useFullTextSearch) {
|
||||
if (mb_strpos($textFilter, '*') !== false) {
|
||||
$skipWidlcards = true;
|
||||
$textFilter = str_replace('*', '%', $textFilter);
|
||||
}
|
||||
}
|
||||
|
||||
$fullTextSearchData = $this->getFullTextSearchDataForTextFilter($textFilter, !$useFullTextSearch);
|
||||
|
||||
$fullTextGroup = [];
|
||||
|
||||
$fullTextSearchFieldList = [];
|
||||
if ($fullTextSearchData) {
|
||||
$group[] = $fullTextSearchData['where'];
|
||||
$fullTextGroup[] = $fullTextSearchData['where'];
|
||||
$fullTextSearchFieldList = $fullTextSearchData['fieldList'];
|
||||
}
|
||||
|
||||
foreach ($fieldList as $field) {
|
||||
if (in_array($field, $fullTextSearchFieldList)) continue;
|
||||
if ($useFullTextSearch) {
|
||||
if (in_array($field, $fullTextSearchFieldList)) continue;
|
||||
}
|
||||
if ($forceFullTextSearch) continue;
|
||||
|
||||
$attributeType = null;
|
||||
if (!empty($fieldDefs[$field]['type'])) {
|
||||
$attributeType = $fieldDefs[$field]['type'];
|
||||
}
|
||||
|
||||
if (
|
||||
strlen($textFilter) >= $textFilterContainsMinLength
|
||||
&&
|
||||
(
|
||||
$attributeType == 'text'
|
||||
||
|
||||
!empty($this->textFilterUseContainsAttributeList[$field])
|
||||
||
|
||||
$attributeType == 'varchar' && $this->getConfig()->get('textFilterUseContainsForVarchar')
|
||||
)
|
||||
) {
|
||||
$expression = '%' . $textFilter . '%';
|
||||
if (!$skipWidlcards) {
|
||||
if (
|
||||
strlen($textFilter) >= $textFilterContainsMinLength
|
||||
&&
|
||||
(
|
||||
$attributeType == 'text'
|
||||
||
|
||||
!empty($this->textFilterUseContainsAttributeList[$field])
|
||||
||
|
||||
$attributeType == 'varchar' && $this->getConfig()->get('textFilterUseContainsForVarchar')
|
||||
)
|
||||
) {
|
||||
$expression = '%' . $textFilter . '%';
|
||||
} else {
|
||||
$expression = $textFilter . '%';
|
||||
}
|
||||
} else {
|
||||
$expression = $textFilter . '%';
|
||||
$expression = $textFilter;
|
||||
}
|
||||
|
||||
if ($fullTextSearchData) {
|
||||
if (!$useFullTextSearch) {
|
||||
if (in_array($field, $fullTextSearchFieldList)) {
|
||||
if (!array_key_exists('OR', $fullTextGroup)) {
|
||||
$fullTextGroup['OR'] = [];
|
||||
}
|
||||
$fullTextGroup['OR'][$field . '*'] = $expression;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$group[$field . '*'] = $expression;
|
||||
}
|
||||
|
||||
if (!empty($fullTextGroup)) {
|
||||
$group['AND'] = $fullTextGroup;
|
||||
}
|
||||
|
||||
if (count($group) === 0) {
|
||||
$result['whereClause'][] = [
|
||||
'id' => null
|
||||
];
|
||||
}
|
||||
|
||||
$result['whereClause'][] = [
|
||||
'OR' => $group
|
||||
];
|
||||
|
||||
@@ -340,8 +340,10 @@ abstract class Base
|
||||
|
||||
$columnList = explode(',', $columns);
|
||||
|
||||
$tableName = $this->toDb($entity->getEntityType());
|
||||
|
||||
foreach ($columnList as $i => $column) {
|
||||
$columnList[$i] = $this->sanitize($column);
|
||||
$columnList[$i] = $tableName . '.' . $this->sanitize($column);
|
||||
}
|
||||
|
||||
$query = $this->quote($query);
|
||||
|
||||
@@ -82,7 +82,7 @@ class GlobalSearch extends \Espo\Core\Services\Base
|
||||
if (!$this->getAcl()->checkScope($entityType, 'read')) {
|
||||
continue;
|
||||
}
|
||||
if (!$this->getMetadata()->get('scopes.' . $entityType)) {
|
||||
if (!$this->getMetadata()->get(['scopes', $entityType])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -98,12 +98,13 @@ class GlobalSearch extends \Espo\Core\Services\Base
|
||||
$hasFullTextSearch = true;
|
||||
$params['select'][] = [$fullTextSearchData['where'], '_relevance'];
|
||||
} else {
|
||||
$params['select'][] = ['VALUE:0.9', '_relevance'];
|
||||
$params['select'][] = ['VALUE:1.1', '_relevance'];
|
||||
$relevanceSelectPosition = count($params['select']);
|
||||
}
|
||||
|
||||
$selectManager->manageAccess($params);
|
||||
$selectManager->manageTextFilter($query, $params);
|
||||
$params['forceFullTextSearch'] = true;
|
||||
$selectManager->applyTextFilter($query, $params);
|
||||
|
||||
$sql = $this->getEntityManager()->getQuery()->createSelectQuery($entityType, $params);
|
||||
|
||||
|
||||
@@ -455,7 +455,7 @@ class QueryTest extends \PHPUnit\Framework\TestCase
|
||||
|
||||
$expectedSql =
|
||||
"SELECT article.id AS `id`, article.name AS `name` FROM `article` " .
|
||||
"WHERE MATCH (name,description) AGAINST ('test +hello' IN BOOLEAN MODE) AND article.id IS NOT NULL AND article.deleted = '0'";
|
||||
"WHERE MATCH (article.name,article.description) AGAINST ('test +hello' IN BOOLEAN MODE) AND article.id IS NOT NULL AND article.deleted = '0'";
|
||||
|
||||
$this->assertEquals($expectedSql, $sql);
|
||||
}
|
||||
@@ -471,7 +471,7 @@ class QueryTest extends \PHPUnit\Framework\TestCase
|
||||
|
||||
$expectedSql =
|
||||
"SELECT article.id AS `id`, article.name AS `name` FROM `article` " .
|
||||
"WHERE MATCH (description) AGAINST ('\"test hello\"' IN NATURAL LANGUAGE MODE) AND article.deleted = '0'";
|
||||
"WHERE MATCH (article.description) AGAINST ('\"test hello\"' IN NATURAL LANGUAGE MODE) AND article.deleted = '0'";
|
||||
|
||||
$this->assertEquals($expectedSql, $sql);
|
||||
}
|
||||
@@ -489,8 +489,8 @@ class QueryTest extends \PHPUnit\Framework\TestCase
|
||||
]);
|
||||
|
||||
$expectedSql =
|
||||
"SELECT article.id AS `id`, MATCH (description) AGAINST ('test' IN BOOLEAN MODE) AS `MATCH_BOOLEAN:description:test` FROM `article` " .
|
||||
"WHERE MATCH (description) AGAINST ('test' IN BOOLEAN MODE) AND article.deleted = '0' " .
|
||||
"SELECT article.id AS `id`, MATCH (article.description) AGAINST ('test' IN BOOLEAN MODE) AS `MATCH_BOOLEAN:description:test` FROM `article` " .
|
||||
"WHERE MATCH (article.description) AGAINST ('test' IN BOOLEAN MODE) AND article.deleted = '0' " .
|
||||
"ORDER BY 2 DESC";
|
||||
|
||||
$this->assertEquals($expectedSql, $sql);
|
||||
@@ -509,10 +509,26 @@ class QueryTest extends \PHPUnit\Framework\TestCase
|
||||
]);
|
||||
|
||||
$expectedSql =
|
||||
"SELECT article.id AS `id`, MATCH (description) AGAINST ('test' IN BOOLEAN MODE) AS `relevance` FROM `article` " .
|
||||
"WHERE MATCH (description) AGAINST ('test' IN BOOLEAN MODE) AND article.deleted = '0' " .
|
||||
"SELECT article.id AS `id`, MATCH (article.description) AGAINST ('test' IN BOOLEAN MODE) AS `relevance` FROM `article` " .
|
||||
"WHERE MATCH (article.description) AGAINST ('test' IN BOOLEAN MODE) AND article.deleted = '0' " .
|
||||
"ORDER BY 2 DESC";
|
||||
|
||||
$this->assertEquals($expectedSql, $sql);
|
||||
}
|
||||
|
||||
public function testMatch5()
|
||||
{
|
||||
$sql = $this->query->createSelectQuery('Article', [
|
||||
'select' => ['id', 'name'],
|
||||
'whereClause' => [
|
||||
'MATCH_NATURAL_LANGUAGE:description:test>' => 1
|
||||
]
|
||||
]);
|
||||
|
||||
$expectedSql =
|
||||
"SELECT article.id AS `id`, article.name AS `name` FROM `article` " .
|
||||
"WHERE MATCH (article.description) AGAINST ('test' IN NATURAL LANGUAGE MODE) > '1' AND article.deleted = '0'";
|
||||
|
||||
$this->assertEquals($expectedSql, $sql);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user