Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 98
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
SearchPostgres
0.00% covered (danger)
0.00%
0 / 97
0.00% covered (danger)
0.00%
0 / 6
380
0.00% covered (danger)
0.00%
0 / 1
 doSearchTitleInDB
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 doSearchTextInDB
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 parseQuery
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
72
 searchQuery
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
56
 update
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
2
 updateTitle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * PostgreSQL search engine
4 *
5 * Copyright © 2006-2007 Greg Sabino Mullane <greg@turnstep.com>
6 * https://www.mediawiki.org/
7 *
8 * @license GPL-2.0-or-later
9 * @file
10 * @ingroup Search
11 */
12
13namespace MediaWiki\Search;
14
15use MediaWiki\MediaWikiServices;
16use MediaWiki\Revision\SlotRecord;
17use Wikimedia\Rdbms\IDatabase;
18
19/**
20 * Search engine hook base class for Postgres
21 * @ingroup Search
22 */
23class SearchPostgres extends SearchDatabase {
24    /**
25     * Perform a full text search query via tsearch2 and return a result set.
26     * Currently searches a page's current title (page.page_title) and
27     * latest revision article text (text.old_text)
28     *
29     * @param string $term Raw search term
30     * @return SqlSearchResultSet
31     */
32    protected function doSearchTitleInDB( $term ) {
33        $q = $this->searchQuery( $term, 'titlevector' );
34        $olderror = error_reporting( E_ERROR );
35        $dbr = $this->dbProvider->getReplicaDatabase();
36        // The real type is still IDatabase, but IReplicaDatabase is used for safety.
37        '@phan-var IDatabase $dbr';
38        // phpcs:ignore MediaWiki.Usage.DbrQueryUsage.DbrQueryFound
39        $resultSet = $dbr->query( $q, 'SearchPostgres', IDatabase::QUERY_SILENCE_ERRORS );
40        error_reporting( $olderror );
41        return new SqlSearchResultSet( $resultSet, $this->searchTerms );
42    }
43
44    /** @inheritDoc */
45    protected function doSearchTextInDB( $term ) {
46        $q = $this->searchQuery( $term, 'textvector' );
47        $olderror = error_reporting( E_ERROR );
48        $dbr = $this->dbProvider->getReplicaDatabase();
49        // The real type is still IDatabase, but IReplicaDatabase is used for safety.
50        '@phan-var IDatabase $dbr';
51        // phpcs:ignore MediaWiki.Usage.DbrQueryUsage.DbrQueryFound
52        $resultSet = $dbr->query( $q, 'SearchPostgres', IDatabase::QUERY_SILENCE_ERRORS );
53        error_reporting( $olderror );
54        return new SqlSearchResultSet( $resultSet, $this->searchTerms );
55    }
56
57    /**
58     * Transform the user's search string into a better form for tsearch2
59     * Returns an SQL fragment consisting of quoted text to search for.
60     *
61     * @param string $term
62     *
63     * @return string
64     */
65    private function parseQuery( $term ) {
66        wfDebug( "parseQuery received: $term" );
67
68        // No backslashes allowed
69        $term = preg_replace( '/\\\\/', '', $term );
70
71        // Collapse parens into nearby words:
72        $term = preg_replace( '/\s*\(\s*/', ' (', $term );
73        $term = preg_replace( '/\s*\)\s*/', ') ', $term );
74
75        // Treat colons as word separators:
76        $term = preg_replace( '/:/', ' ', $term );
77
78        $searchstring = '';
79        $m = [];
80        if ( preg_match_all( '/([-!]?)(\S+)\s*/', $term, $m, PREG_SET_ORDER ) ) {
81            foreach ( $m as $terms ) {
82                if ( $terms[1] !== '' ) {
83                    $searchstring .= ' & !';
84                }
85                if ( strtolower( $terms[2] ) === 'and' ) {
86                    $searchstring .= ' & ';
87                } elseif ( strtolower( $terms[2] ) === 'or' || $terms[2] === '|' ) {
88                    $searchstring .= ' | ';
89                } elseif ( strtolower( $terms[2] ) === 'not' ) {
90                    $searchstring .= ' & !';
91                } else {
92                    $searchstring .= " & $terms[2]";
93                }
94            }
95        }
96
97        // Strip out leading junk
98        $searchstring = preg_replace( '/^[\s\&\|]+/', '', $searchstring );
99
100        // Remove any doubled-up operators
101        $searchstring = preg_replace( '/([\!\&\|]) +(?:[\&\|] +)+/', "$1 ", $searchstring );
102
103        // Remove any non-spaced operators (e.g. "Zounds!")
104        $searchstring = preg_replace( '/([^ ])[\!\&\|]/', "$1", $searchstring );
105
106        // Remove any trailing whitespace or operators
107        $searchstring = preg_replace( '/[\s\!\&\|]+$/', '', $searchstring );
108
109        // Remove unnecessary quotes around everything
110        $searchstring = preg_replace( '/^[\'"](.*)[\'"]$/', "$1", $searchstring );
111
112        // Quote the whole thing
113        $dbr = $this->dbProvider->getReplicaDatabase();
114        $searchstring = $dbr->addQuotes( $searchstring );
115
116        wfDebug( "parseQuery returned: $searchstring" );
117
118        return $searchstring;
119    }
120
121    /**
122     * Construct the full SQL query to do the search.
123     * @param string $term
124     * @param string $fulltext
125     * @return string
126     */
127    private function searchQuery( $term, $fulltext ) {
128        # Get the SQL fragment for the given term
129        $searchstring = $this->parseQuery( $term );
130
131        // We need a separate query here so gin does not complain about empty searches
132        $sql = "SELECT to_tsquery($searchstring)";
133        $dbr = $this->dbProvider->getReplicaDatabase();
134        // The real type is still IDatabase, but IReplicaDatabase is used for safety.
135        '@phan-var IDatabase $dbr';
136        // phpcs:ignore MediaWiki.Usage.DbrQueryUsage.DbrQueryFound
137        $res = $dbr->query( $sql, __METHOD__ );
138        if ( !$res ) {
139            // TODO: Better output (example to catch: one 'two)
140            die( "Sorry, that was not a valid search string. Please go back and try again" );
141        }
142        $top = $res->fetchRow()[0];
143
144        $this->searchTerms = [];
145        $slotRoleStore = MediaWikiServices::getInstance()->getSlotRoleStore();
146        if ( $top === "" ) { // e.g. if only stopwords are used XXX return something better
147            $query = "SELECT page_id, page_namespace, page_title, 0 AS score " .
148                "FROM page p, revision r, slots s, content c, \"text\" pc " .
149                "WHERE p.page_latest = r.rev_id " .
150                "AND s.slot_revision_id = r.rev_id " .
151                "AND s.slot_role_id = " .
152                    $dbr->addQuotes( $slotRoleStore->acquireId( SlotRecord::MAIN ) ) . " " .
153                "AND c.content_id = s.slot_content_id " .
154                "AND pc.old_id = substring( c.content_address from '^tt:([0-9]+)$' )::int " .
155                "AND 1=0";
156        } else {
157            $m = [];
158            if ( preg_match_all( "/'([^']+)'/", $top, $m, PREG_SET_ORDER ) ) {
159                foreach ( $m as $terms ) {
160                    $this->searchTerms[$terms[1]] = $terms[1];
161                }
162            }
163
164            $query = "SELECT page_id, page_namespace, page_title, " .
165                "ts_rank($fulltext, to_tsquery($searchstring), 5) AS score " .
166                "FROM page p, revision r, slots s, content c, \"text\" pc " .
167                "WHERE p.page_latest = r.rev_id " .
168                "AND s.slot_revision_id = r.rev_id " .
169                "AND s.slot_role_id = " . $dbr->addQuotes(
170                    $slotRoleStore->acquireId( SlotRecord::MAIN ) ) . " " .
171                "AND c.content_id = s.slot_content_id " .
172                "AND pc.old_id = substring( c.content_address from '^tt:([0-9]+)$' )::int " .
173                "AND $fulltext @@ to_tsquery($searchstring)";
174        }
175        // Namespaces - defaults to main
176        if ( $this->namespaces !== null ) { // null -> search all
177            if ( count( $this->namespaces ) < 1 ) {
178                $query .= ' AND page_namespace = ' . NS_MAIN;
179            } else {
180                $namespaces = $dbr->makeList( $this->namespaces );
181                $query .= " AND page_namespace IN ($namespaces)";
182            }
183        }
184
185        $query .= " ORDER BY score DESC, page_id DESC";
186
187        $query .= $dbr->limitResult( '', $this->limit, $this->offset );
188
189        wfDebug( "searchQuery returned: $query" );
190
191        return $query;
192    }
193
194    // Most of the work of these two functions are done automatically via triggers
195
196    /** @inheritDoc */
197    public function update( $pageid, $title, $text ) {
198        // We don't want to index older revisions
199        $slotRoleStore = MediaWikiServices::getInstance()->getSlotRoleStore();
200        $dbw = $this->dbProvider->getPrimaryDatabase();
201        $sql = "UPDATE \"text\" SET textvector = NULL " .
202            "WHERE textvector IS NOT NULL " .
203            "AND old_id IN " .
204            "(SELECT DISTINCT substring( c.content_address from '^tt:([0-9]+)$' )::int AS old_rev_text_id " .
205            " FROM content c, slots s, revision r " .
206            " WHERE r.rev_page = $pageid " .
207            " AND s.slot_revision_id = r.rev_id " .
208            " AND s.slot_role_id = " .
209                $dbw->addQuotes( $slotRoleStore->acquireId( SlotRecord::MAIN ) ) . " " .
210            " AND c.content_id = s.slot_content_id " .
211            " ORDER BY old_rev_text_id DESC OFFSET 1)";
212
213        $dbw->query( $sql, __METHOD__ );
214
215        return true;
216    }
217
218    /** @inheritDoc */
219    public function updateTitle( $id, $title ) {
220        return true;
221    }
222}
223
224/** @deprecated class alias since 1.46 */
225class_alias( SearchPostgres::class, 'SearchPostgres' );