b101b3e2d908ffe49983ec986e59771553132246
[Packages/TYPO3.CMS.git] / typo3 / sysext / indexed_search_mysql / Classes / Hook / MysqlFulltextIndexHook.php
1 <?php
2 namespace TYPO3\CMS\IndexedSearchMysql\Hook;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 /**
18 * Class that hooks into Indexed Search and replaces standard SQL queries with MySQL fulltext index queries.
19 */
20 class MysqlFulltextIndexHook
21 {
22 /**
23 * @var \TYPO3\CMS\IndexedSearch\Controller\SearchFormController|\TYPO3\CMS\IndexedSearch\Domain\Repository\IndexSearchRepository
24 */
25 public $pObj;
26
27 const ANY_PART_OF_THE_WORD = '1';
28 const LAST_PART_OF_THE_WORD = '2';
29 const FIRST_PART_OF_THE_WORD = '3';
30 const SOUNDS_LIKE = '10';
31 const SENTENCE = '20';
32 /**
33 * Gets a SQL result pointer to traverse for the search records.
34 *
35 * @param array $searchWordsArray Search words
36 * @param int $freeIndexUid Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
37 * @return bool|\mysqli_result|object MySQLi result object / DBAL object
38 */
39 public function getResultRows_SQLpointer($searchWordsArray, $freeIndexUid = -1)
40 {
41 // Build the search string, detect which fulltext index to use, and decide whether boolean search is needed or not
42 $searchData = $this->getSearchString($searchWordsArray);
43 // Perform SQL Search / collection of result rows array:
44 $resource = false;
45 if ($searchData) {
46 // Do the search:
47 $GLOBALS['TT']->push('execFinalQuery');
48 $resource = $this->execFinalQuery_fulltext($searchData, $freeIndexUid);
49 $GLOBALS['TT']->pull();
50 }
51 return $resource;
52 }
53
54 /**
55 * Returns a search string for use with MySQL FULLTEXT query
56 *
57 * @param array $searchWordArray Search word array
58 * @return string Search string
59 */
60 public function getSearchString($searchWordArray)
61 {
62 // Initialize variables:
63 $count = 0;
64 // Change this to TRUE to force BOOLEAN SEARCH MODE (useful if fulltext index is still empty)
65 $searchBoolean = false;
66 $fulltextIndex = 'index_fulltext.fulltextdata';
67 // This holds the result if the search is natural (doesn't contain any boolean operators)
68 $naturalSearchString = '';
69 // This holds the result if the search is boolen (contains +/-/| operators)
70 $booleanSearchString = '';
71
72 $searchType = (string)$this->pObj->getSearchType();
73
74 // Traverse searchwords and prefix them with corresponding operator
75 foreach ($searchWordArray as $searchWordData) {
76 // Making the query for a single search word based on the search-type
77 $searchWord = $searchWordData['sword'];
78 $wildcard = '';
79 if (strstr($searchWord, ' ')) {
80 $searchType = self::SENTENCE;
81 }
82 switch ($searchType) {
83 case self::ANY_PART_OF_THE_WORD:
84
85 case self::LAST_PART_OF_THE_WORD:
86
87 case self::FIRST_PART_OF_THE_WORD:
88 // First part of word
89 $wildcard = '*';
90 // Part-of-word search requires boolean mode!
91 $searchBoolean = true;
92 break;
93 case self::SOUNDS_LIKE:
94 $indexerObj = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\IndexedSearch\Indexer::class);
95 // Initialize the indexer-class
96 /** @var \TYPO3\CMS\IndexedSearch\Indexer $indexerObj */
97 $searchWord = $indexerObj->metaphone($searchWord, $indexerObj->storeMetaphoneInfoAsWords);
98 unset($indexerObj);
99 $fulltextIndex = 'index_fulltext.metaphonedata';
100 break;
101 case self::SENTENCE:
102 $searchBoolean = true;
103 // Remove existing quotes and fix misplaced quotes.
104 $searchWord = trim(str_replace('"', ' ', $searchWord));
105 break;
106 }
107 // Perform search for word:
108 switch ($searchWordData['oper']) {
109 case 'AND NOT':
110 $booleanSearchString .= ' -' . $searchWord . $wildcard;
111 $searchBoolean = true;
112 break;
113 case 'OR':
114 $booleanSearchString .= ' ' . $searchWord . $wildcard;
115 $searchBoolean = true;
116 break;
117 default:
118 $booleanSearchString .= ' +' . $searchWord . $wildcard;
119 $naturalSearchString .= ' ' . $searchWord;
120 }
121 $count++;
122 }
123 if ($searchType == self::SENTENCE) {
124 $searchString = '"' . trim($naturalSearchString) . '"';
125 } elseif ($searchBoolean) {
126 $searchString = trim($booleanSearchString);
127 } else {
128 $searchString = trim($naturalSearchString);
129 }
130 return array(
131 'searchBoolean' => $searchBoolean,
132 'searchString' => $searchString,
133 'fulltextIndex' => $fulltextIndex
134 );
135 }
136
137 /**
138 * Execute final query, based on phash integer list. The main point is sorting the result in the right order.
139 *
140 * @param array $searchData Array with search string, boolean indicator, and fulltext index reference
141 * @param int $freeIndexUid Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
142 * @return bool|\mysqli_result|object MySQLi result object / DBAL object
143 */
144 protected function execFinalQuery_fulltext($searchData, $freeIndexUid = -1)
145 {
146 // Setting up methods of filtering results based on page types, access, etc.
147 $pageJoin = '';
148 // Indexing configuration clause:
149 $freeIndexUidClause = $this->pObj->freeIndexUidWhere($freeIndexUid);
150 // Calling hook for alternative creation of page ID list
151 $searchRootPageIdList = $this->pObj->getSearchRootPageIdList();
152 if ($hookObj = &$this->pObj->hookRequest('execFinalQuery_idList')) {
153 $pageWhere = $hookObj->execFinalQuery_idList('');
154 } elseif ($this->pObj->getJoinPagesForQuery()) {
155 // Alternative to getting all page ids by ->getTreeList() where "excludeSubpages" is NOT respected.
156 $pageJoin = ',
157 pages';
158 $pageWhere = 'pages.uid = ISEC.page_id
159 ' . $GLOBALS['TSFE']->cObj->enableFields('pages') . '
160 AND pages.no_search=0
161 AND pages.doktype<200
162 ';
163 } elseif ($searchRootPageIdList[0] >= 0) {
164
165 // Collecting all pages IDs in which to search;
166 // filtering out ALL pages that are not accessible due to enableFields. Does NOT look for "no_search" field!
167 $idList = array();
168 foreach ($searchRootPageIdList as $rootId) {
169 /** @var \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer $cObj */
170 $cObj = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::class);
171 $idList[] = $cObj->getTreeList(-1 * $rootId, 9999);
172 }
173 $pageWhere = ' ISEC.page_id IN (' . implode(',', $idList) . ')';
174 } else {
175 // Disable everything... (select all)
176 $pageWhere = ' 1=1';
177 }
178 $searchBoolean = '';
179 if ($searchData['searchBoolean']) {
180 $searchBoolean = ' IN BOOLEAN MODE';
181 }
182 $resource = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
183 'index_fulltext.*, ISEC.*, IP.*',
184 'index_fulltext, index_section ISEC, index_phash IP' . $pageJoin,
185 'MATCH (' . $searchData['fulltextIndex'] . ')
186 AGAINST (' . $GLOBALS['TYPO3_DB']->fullQuoteStr($searchData['searchString'], 'index_fulltext') . $searchBoolean . ') ' .
187 $this->pObj->mediaTypeWhere() . ' ' . $this->pObj->languageWhere() . $freeIndexUidClause . '
188 AND index_fulltext.phash = IP.phash
189 AND ISEC.phash = IP.phash
190 AND ' . $pageWhere . $this->pObj->sectionTableWhere(),
191 'IP.phash,ISEC.phash,ISEC.phash_t3,ISEC.rl0,ISEC.rl1,ISEC.rl2,ISEC.page_id,ISEC.uniqid,IP.phash_grouping,IP.data_filename ,IP.data_page_id ,IP.data_page_reg1,IP.data_page_type,IP.data_page_mp,IP.gr_list,IP.item_type,IP.item_title,IP.item_description,IP.item_mtime,IP.tstamp,IP.item_size,IP.contentHash,IP.crdate,IP.parsetime,IP.sys_language_uid,IP.item_crdate,IP.cHashParams,IP.externalUrl,IP.recordUid,IP.freeIndexUid,IP.freeIndexSetId'
192 );
193 return $resource;
194 }
195 }