]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/search/SearchSqlite.php
MediaWiki 1.30.2-scripts2
[autoinstalls/mediawiki.git] / includes / search / SearchSqlite.php
1 <?php
2 /**
3  * SQLite search backend, based upon SearchMysql
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  * http://www.gnu.org/copyleft/gpl.html
19  *
20  * @file
21  * @ingroup Search
22  */
23
24 /**
25  * Search engine hook for SQLite
26  * @ingroup Search
27  */
28 class SearchSqlite extends SearchDatabase {
29         /**
30          * Whether fulltext search is supported by current schema
31          * @return bool
32          */
33         function fulltextSearchSupported() {
34                 return $this->db->checkForEnabledSearch();
35         }
36
37         /**
38          * Parse the user's query and transform it into an SQL fragment which will
39          * become part of a WHERE clause
40          *
41          * @param string $filteredText
42          * @param bool $fulltext
43          * @return string
44          */
45         function parseQuery( $filteredText, $fulltext ) {
46                 global $wgContLang;
47                 $lc = $this->legalSearchChars( self::CHARS_NO_SYNTAX ); // Minus syntax chars (" and *)
48                 $searchon = '';
49                 $this->searchTerms = [];
50
51                 $m = [];
52                 if ( preg_match_all( '/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/',
53                                 $filteredText, $m, PREG_SET_ORDER ) ) {
54                         foreach ( $m as $bits ) {
55                                 MediaWiki\suppressWarnings();
56                                 list( /* all */, $modifier, $term, $nonQuoted, $wildcard ) = $bits;
57                                 MediaWiki\restoreWarnings();
58
59                                 if ( $nonQuoted != '' ) {
60                                         $term = $nonQuoted;
61                                         $quote = '';
62                                 } else {
63                                         $term = str_replace( '"', '', $term );
64                                         $quote = '"';
65                                 }
66
67                                 if ( $searchon !== '' ) {
68                                         $searchon .= ' ';
69                                 }
70
71                                 // Some languages such as Serbian store the input form in the search index,
72                                 // so we may need to search for matches in multiple writing system variants.
73                                 $convertedVariants = $wgContLang->autoConvertToAllVariants( $term );
74                                 if ( is_array( $convertedVariants ) ) {
75                                         $variants = array_unique( array_values( $convertedVariants ) );
76                                 } else {
77                                         $variants = [ $term ];
78                                 }
79
80                                 // The low-level search index does some processing on input to work
81                                 // around problems with minimum lengths and encoding in MySQL's
82                                 // fulltext engine.
83                                 // For Chinese this also inserts spaces between adjacent Han characters.
84                                 $strippedVariants = array_map(
85                                         [ $wgContLang, 'normalizeForSearch' ],
86                                         $variants );
87
88                                 // Some languages such as Chinese force all variants to a canonical
89                                 // form when stripping to the low-level search index, so to be sure
90                                 // let's check our variants list for unique items after stripping.
91                                 $strippedVariants = array_unique( $strippedVariants );
92
93                                 $searchon .= $modifier;
94                                 if ( count( $strippedVariants ) > 1 ) {
95                                         $searchon .= '(';
96                                 }
97                                 foreach ( $strippedVariants as $stripped ) {
98                                         if ( $nonQuoted && strpos( $stripped, ' ' ) !== false ) {
99                                                 // Hack for Chinese: we need to toss in quotes for
100                                                 // multiple-character phrases since normalizeForSearch()
101                                                 // added spaces between them to make word breaks.
102                                                 $stripped = '"' . trim( $stripped ) . '"';
103                                         }
104                                         $searchon .= "$quote$stripped$quote$wildcard ";
105                                 }
106                                 if ( count( $strippedVariants ) > 1 ) {
107                                         $searchon .= ')';
108                                 }
109
110                                 // Match individual terms or quoted phrase in result highlighting...
111                                 // Note that variants will be introduced in a later stage for highlighting!
112                                 $regexp = $this->regexTerm( $term, $wildcard );
113                                 $this->searchTerms[] = $regexp;
114                         }
115
116                 } else {
117                         wfDebug( __METHOD__ . ": Can't understand search query '{$filteredText}'\n" );
118                 }
119
120                 $searchon = $this->db->addQuotes( $searchon );
121                 $field = $this->getIndexField( $fulltext );
122                 return " $field MATCH $searchon ";
123         }
124
125         function regexTerm( $string, $wildcard ) {
126                 global $wgContLang;
127
128                 $regex = preg_quote( $string, '/' );
129                 if ( $wgContLang->hasWordBreaks() ) {
130                         if ( $wildcard ) {
131                                 // Don't cut off the final bit!
132                                 $regex = "\b$regex";
133                         } else {
134                                 $regex = "\b$regex\b";
135                         }
136                 } else {
137                         // For Chinese, words may legitimately abut other words in the text literal.
138                         // Don't add \b boundary checks... note this could cause false positives
139                         // for latin chars.
140                 }
141                 return $regex;
142         }
143
144         public static function legalSearchChars( $type = self::CHARS_ALL ) {
145                 $searchChars = parent::legalSearchChars( $type );
146                 if ( $type === self::CHARS_ALL ) {
147                         // " for phrase, * for wildcard
148                         $searchChars = "\"*" . $searchChars;
149                 }
150                 return $searchChars;
151         }
152
153         /**
154          * Perform a full text search query and return a result set.
155          *
156          * @param string $term Raw search term
157          * @return SqlSearchResultSet
158          */
159         function searchText( $term ) {
160                 return $this->searchInternal( $term, true );
161         }
162
163         /**
164          * Perform a title-only search query and return a result set.
165          *
166          * @param string $term Raw search term
167          * @return SqlSearchResultSet
168          */
169         function searchTitle( $term ) {
170                 return $this->searchInternal( $term, false );
171         }
172
173         protected function searchInternal( $term, $fulltext ) {
174                 global $wgContLang;
175
176                 if ( !$this->fulltextSearchSupported() ) {
177                         return null;
178                 }
179
180                 $filteredTerm = $this->filter( $wgContLang->lc( $term ) );
181                 $resultSet = $this->db->query( $this->getQuery( $filteredTerm, $fulltext ) );
182
183                 $total = null;
184                 $totalResult = $this->db->query( $this->getCountQuery( $filteredTerm, $fulltext ) );
185                 $row = $totalResult->fetchObject();
186                 if ( $row ) {
187                         $total = intval( $row->c );
188                 }
189                 $totalResult->free();
190
191                 return new SqlSearchResultSet( $resultSet, $this->searchTerms, $total );
192         }
193
194         /**
195          * Return a partial WHERE clause to limit the search to the given namespaces
196          * @return string
197          */
198         function queryNamespaces() {
199                 if ( is_null( $this->namespaces ) ) {
200                         return '';  # search all
201                 }
202                 if ( !count( $this->namespaces ) ) {
203                         $namespaces = '0';
204                 } else {
205                         $namespaces = $this->db->makeList( $this->namespaces );
206                 }
207                 return 'AND page_namespace IN (' . $namespaces . ')';
208         }
209
210         /**
211          * Returns a query with limit for number of results set.
212          * @param string $sql
213          * @return string
214          */
215         function limitResult( $sql ) {
216                 return $this->db->limitResult( $sql, $this->limit, $this->offset );
217         }
218
219         /**
220          * Construct the full SQL query to do the search.
221          * The guts shoulds be constructed in queryMain()
222          * @param string $filteredTerm
223          * @param bool $fulltext
224          * @return string
225          */
226         function getQuery( $filteredTerm, $fulltext ) {
227                 return $this->limitResult(
228                         $this->queryMain( $filteredTerm, $fulltext ) . ' ' .
229                         $this->queryNamespaces()
230                 );
231         }
232
233         /**
234          * Picks which field to index on, depending on what type of query.
235          * @param bool $fulltext
236          * @return string
237          */
238         function getIndexField( $fulltext ) {
239                 return $fulltext ? 'si_text' : 'si_title';
240         }
241
242         /**
243          * Get the base part of the search query.
244          *
245          * @param string $filteredTerm
246          * @param bool $fulltext
247          * @return string
248          */
249         function queryMain( $filteredTerm, $fulltext ) {
250                 $match = $this->parseQuery( $filteredTerm, $fulltext );
251                 $page = $this->db->tableName( 'page' );
252                 $searchindex = $this->db->tableName( 'searchindex' );
253                 return "SELECT $searchindex.rowid, page_namespace, page_title " .
254                         "FROM $page,$searchindex " .
255                         "WHERE page_id=$searchindex.rowid AND $match";
256         }
257
258         function getCountQuery( $filteredTerm, $fulltext ) {
259                 $match = $this->parseQuery( $filteredTerm, $fulltext );
260                 $page = $this->db->tableName( 'page' );
261                 $searchindex = $this->db->tableName( 'searchindex' );
262                 return "SELECT COUNT(*) AS c " .
263                         "FROM $page,$searchindex " .
264                         "WHERE page_id=$searchindex.rowid AND $match " .
265                         $this->queryNamespaces();
266         }
267
268         /**
269          * Create or update the search index record for the given page.
270          * Title and text should be pre-processed.
271          *
272          * @param int $id
273          * @param string $title
274          * @param string $text
275          */
276         function update( $id, $title, $text ) {
277                 if ( !$this->fulltextSearchSupported() ) {
278                         return;
279                 }
280                 // @todo find a method to do it in a single request,
281                 // couldn't do it so far due to typelessness of FTS3 tables.
282                 $dbw = wfGetDB( DB_MASTER );
283
284                 $dbw->delete( 'searchindex', [ 'rowid' => $id ], __METHOD__ );
285
286                 $dbw->insert( 'searchindex',
287                         [
288                                 'rowid' => $id,
289                                 'si_title' => $title,
290                                 'si_text' => $text
291                         ], __METHOD__ );
292         }
293
294         /**
295          * Update a search index record's title only.
296          * Title should be pre-processed.
297          *
298          * @param int $id
299          * @param string $title
300          */
301         function updateTitle( $id, $title ) {
302                 if ( !$this->fulltextSearchSupported() ) {
303                         return;
304                 }
305                 $dbw = wfGetDB( DB_MASTER );
306
307                 $dbw->update( 'searchindex',
308                         [ 'si_title' => $title ],
309                         [ 'rowid' => $id ],
310                         __METHOD__ );
311         }
312 }