Skip to content

Commit ad5de3e

Browse files
committed
fix: added missing PDO based database search classes
1 parent 5d5ec3c commit ad5de3e

File tree

6 files changed

+430
-7
lines changed

6 files changed

+430
-7
lines changed

phpmyfaq/src/phpMyFAQ/Search/Database/DatabaseInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22

33
/**
4-
* Interface for phpMyFAQ database dependent search classes.
4+
* Interface for phpMyFAQ database-dependent search classes.
55
*
66
* This Source Code Form is subject to the terms of the Mozilla Public License,
77
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<?php
2+
3+
/**
4+
* phpMyFAQ MySQL (PDO_MYSQL) search classes.
5+
*
6+
* This Source Code Form is subject to the terms of the Mozilla Public License,
7+
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
8+
* obtain one at https://mozilla.org/MPL/2.0/.
9+
*
10+
* @package phpMyFAQ
11+
* @author Thorsten Rinne <[email protected]>
12+
* @copyright 2025 phpMyFAQ Team
13+
* @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14+
* @link https://www.phpmyfaq.de
15+
* @since 2025-02-12
16+
*/
17+
18+
namespace phpMyFAQ\Search\Database;
19+
20+
use phpMyFAQ\Configuration;
21+
use phpMyFAQ\Search\SearchDatabase;
22+
23+
/**
24+
* Class PdoMysql
25+
*
26+
* @package phpMyFAQ\Search\Database
27+
*/
28+
class PdoMysql extends SearchDatabase implements DatabaseInterface
29+
{
30+
/**
31+
* Constructor.
32+
*/
33+
public function __construct(Configuration $configuration)
34+
{
35+
parent::__construct($configuration);
36+
$this->relevanceSupport = true;
37+
}
38+
39+
/**
40+
* Prepares the search and executes it.
41+
*
42+
* @param string $searchTerm Search term
43+
* @throws \Exception
44+
*/
45+
public function search(string $searchTerm): mixed
46+
{
47+
if (is_numeric($searchTerm) && $this->configuration->get('search.searchForSolutionId')) {
48+
return parent::search($searchTerm);
49+
}
50+
$relevance = $this->configuration->get('search.enableRelevance');
51+
$columns = $this->getResultColumns();
52+
if ($this->relevanceSupport && $relevance) {
53+
$columns .= ', ' . $this->setRelevanceRanking($searchTerm);
54+
$orderBy = 'ORDER BY score DESC';
55+
} else {
56+
$orderBy = '';
57+
}
58+
$chars = [
59+
"\xe2\x80\x98",
60+
"\xe2\x80\x99",
61+
"\xe2\x80\x9c",
62+
"\xe2\x80\x9d",
63+
"\xe2\x80\x93",
64+
"\xe2\x80\x94",
65+
"\xe2\x80\xa6",
66+
];
67+
$replace = ["'", "'", '"', '"', '-', '--', '...'];
68+
$searchTerm = str_replace($chars, $replace, $searchTerm);
69+
$query = sprintf(
70+
"
71+
SELECT
72+
%s
73+
FROM
74+
%s %s %s
75+
WHERE
76+
MATCH (%s) AGAINST ('%s' IN BOOLEAN MODE)
77+
%s
78+
%s",
79+
$columns,
80+
$this->getTable(),
81+
$this->getJoinedTable(),
82+
$this->getJoinedColumns(),
83+
$this->getMatchingColumns(),
84+
$this->configuration->getDb()->escape($searchTerm),
85+
$this->getConditions(),
86+
$orderBy
87+
);
88+
$this->resultSet = $this->configuration->getDb()->query($query);
89+
// Fallback for searches with less than three characters
90+
if (false !== $this->resultSet && 0 === $this->configuration->getDb()->numRows($this->resultSet)) {
91+
$query = sprintf(
92+
'
93+
SELECT
94+
%s
95+
FROM
96+
%s %s %s
97+
WHERE
98+
%s
99+
%s',
100+
$this->getResultColumns(),
101+
$this->getTable(),
102+
$this->getJoinedTable(),
103+
$this->getJoinedColumns(),
104+
$this->getMatchClause($searchTerm),
105+
$this->getConditions()
106+
);
107+
108+
$this->resultSet = $this->configuration->getDb()->query($query);
109+
}
110+
return $this->resultSet;
111+
}
112+
113+
/**
114+
* Add the matching columns into the columns for the relevance ranking
115+
*/
116+
public function setRelevanceRanking(string $searchTerm): string
117+
{
118+
return sprintf(
119+
"MATCH (%s) AGAINST ('%s' IN BOOLEAN MODE) as score",
120+
$this->getMatchingColumns(),
121+
$this->configuration->getDb()->escape($searchTerm)
122+
);
123+
}
124+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
<?php
2+
3+
/**
4+
* phpMyFAQ PostgreSQL (PDO_PGSQL) search classes.
5+
*
6+
* This Source Code Form is subject to the terms of the Mozilla Public License,
7+
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
8+
* obtain one at https://mozilla.org/MPL/2.0/.
9+
*
10+
* @package phpMyFAQ
11+
* @author Thorsten Rinne <[email protected]>
12+
* @copyright 2025 phpMyFAQ Team
13+
* @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14+
* @link https://www.phpmyfaq.de
15+
* @since 2025-02-12
16+
*/
17+
18+
namespace phpMyFAQ\Search\Database;
19+
20+
use Exception;
21+
use phpMyFAQ\Configuration;
22+
use phpMyFAQ\Search\SearchDatabase;
23+
24+
/**
25+
* Class PdoPgsql
26+
*
27+
* @package phpMyFAQ\Search\Database
28+
*/
29+
class PdoPgsql extends SearchDatabase implements DatabaseInterface
30+
{
31+
/**
32+
* Constructor.
33+
*/
34+
public function __construct(Configuration $configuration)
35+
{
36+
parent::__construct($configuration);
37+
$this->relevanceSupport = true;
38+
}
39+
40+
/**
41+
* Prepares the search and executes it.
42+
*
43+
* @param string $searchTerm Search term
44+
* @throws Exception
45+
*/
46+
public function search(string $searchTerm): mixed
47+
{
48+
if (is_numeric($searchTerm) && $this->configuration->get('search.searchForSolutionId')) {
49+
parent::search($searchTerm);
50+
} else {
51+
$enableRelevance = $this->configuration->get('search.enableRelevance');
52+
53+
$columns = $this->getResultColumns();
54+
$columns .= ($enableRelevance) ? $this->getMatchingColumnsAsResult() : '';
55+
$orderBy = ($enableRelevance) ? 'ORDER BY ' . $this->getMatchingOrder() : '';
56+
57+
$query = sprintf(
58+
"
59+
SELECT
60+
%s
61+
FROM
62+
%s %s %s %s
63+
WHERE
64+
(%s) ILIKE ('%%%s%%')
65+
%s
66+
%s",
67+
$columns,
68+
$this->getTable(),
69+
$this->getJoinedTable(),
70+
$this->getJoinedColumns(),
71+
($enableRelevance)
72+
? ", plainto_tsquery('" . $this->configuration->getDb()->escape($searchTerm) . "') query "
73+
: '',
74+
$this->getMatchingColumns(),
75+
$this->configuration->getDb()->escape($searchTerm),
76+
$this->getConditions(),
77+
$orderBy
78+
);
79+
80+
$this->resultSet = $this->configuration->getDb()->query($query);
81+
}
82+
83+
return $this->resultSet;
84+
}
85+
86+
/**
87+
* Add the matching columns into the columns for the resultset.
88+
*/
89+
public function getMatchingColumnsAsResult(): string
90+
{
91+
$resultColumns = '';
92+
$config = $this->configuration->get('search.relevance');
93+
$list = explode(',', (string) $config);
94+
95+
// Set weight
96+
$weights = ['A', 'B', 'C', 'D'];
97+
$weight = [];
98+
foreach ($list as $columnName) {
99+
$weight[$columnName] = array_shift($weights);
100+
}
101+
102+
foreach ($this->matchingColumns as $matchingColumn) {
103+
$columnName = substr(strstr($matchingColumn, '.'), 1);
104+
105+
if (isset($weight[$columnName])) {
106+
$column = sprintf(
107+
"TS_RANK_CD(SETWEIGHT(TO_TSVECTOR(COALESCE(%s, '')), '%s'), query) AS relevance_%s",
108+
$matchingColumn,
109+
$weight[$columnName],
110+
$columnName
111+
);
112+
113+
$resultColumns .= ', ' . $column;
114+
}
115+
}
116+
117+
return $resultColumns;
118+
}
119+
120+
/**
121+
* Returns the part of the SQL query with the order by.
122+
*
123+
* The order is calculate by weight depend on the search.relevance order
124+
*/
125+
public function getMatchingOrder(): string
126+
{
127+
$list = explode(',', (string) $this->configuration->get('search.relevance'));
128+
$order = '';
129+
130+
foreach ($list as $field) {
131+
$string = sprintf(
132+
'relevance_%s DESC',
133+
$field
134+
);
135+
if ($order === '' || $order === '0') {
136+
$order .= $string;
137+
} else {
138+
$order .= ', ' . $string;
139+
}
140+
}
141+
142+
return $order;
143+
}
144+
145+
/**
146+
* Returns the part of the SQL query with the matching columns.
147+
*/
148+
public function getMatchingColumns(): string
149+
{
150+
$enableRelevance = $this->configuration->get('search.enableRelevance');
151+
152+
if ($enableRelevance) {
153+
$matchColumns = '';
154+
155+
foreach ($this->matchingColumns as $matchingColumn) {
156+
$match = sprintf("to_tsvector(coalesce(%s,''))", $matchingColumn);
157+
if ($matchColumns === '' || $matchColumns === '0') {
158+
$matchColumns .= '(' . $match;
159+
} else {
160+
$matchColumns .= ' || ' . $match;
161+
}
162+
}
163+
164+
// Add the ILIKE since the FULLTEXT looks for the exact phrase only
165+
$matchColumns .= ') @@ query) OR (' . implode(" || ' ' || ", $this->matchingColumns);
166+
} else {
167+
$matchColumns = implode(" || ' ' || ", $this->matchingColumns);
168+
}
169+
170+
return $matchColumns;
171+
}
172+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
/**
4+
* phpMyFAQ SQLite3 (PDO_SQLITE) based search classes.
5+
*
6+
* This Source Code Form is subject to the terms of the Mozilla Public License,
7+
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
8+
* obtain one at https://mozilla.org/MPL/2.0/.
9+
*
10+
* @package phpMyFAQ
11+
*
12+
* @author Thorsten Rinne <[email protected]>
13+
* @copyright 2025 phpMyFAQ Team
14+
* @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
15+
* @link https://www.phpmyfaq.de
16+
* @since 2025-02-12
17+
*/
18+
19+
namespace phpMyFAQ\Search\Database;
20+
21+
use Exception;
22+
use phpMyFAQ\Search\SearchDatabase;
23+
24+
/**
25+
* Class PdoSqlite
26+
*
27+
* @package phpMyFAQ\Search\Database
28+
*/
29+
class PdoSqlite extends SearchDatabase implements DatabaseInterface
30+
{
31+
/**
32+
* Prepares the search and executes it.
33+
*
34+
* @param string $searchTerm Search ter
35+
* @throws Exception
36+
*/
37+
public function search(string $searchTerm): mixed
38+
{
39+
if (is_numeric($searchTerm) && $this->configuration->get('search.searchForSolutionId')) {
40+
parent::search($searchTerm);
41+
} else {
42+
$query = sprintf(
43+
'
44+
SELECT
45+
%s
46+
FROM
47+
%s %s %s
48+
WHERE
49+
%s
50+
%s',
51+
$this->getResultColumns(),
52+
$this->getTable(),
53+
$this->getJoinedTable(),
54+
$this->getJoinedColumns(),
55+
$this->getMatchClause($searchTerm),
56+
$this->getConditions()
57+
);
58+
59+
$this->resultSet = $this->configuration->getDb()->query($query);
60+
}
61+
62+
return $this->resultSet;
63+
}
64+
}

0 commit comments

Comments
 (0)