Skip to content

Commit 519d58c

Browse files
mErilainendrunken-monkey
mErilainen
authored andcommitted
Issue #2971033 by mErilainen, drunken monkey, borisson_: Added an option for prefix matching to the database backend.
1 parent 5a06192 commit 519d58c

File tree

7 files changed

+134
-25
lines changed

7 files changed

+134
-25
lines changed

Diff for: CHANGELOG.txt

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
Search API 1.x, dev (xxxx-xx-xx):
22
---------------------------------
3+
- #2971033 by mErilainen, drunken monkey, borisson_: Added an option for prefix
4+
matching to the database backend.
35
- #2973034 by idebr, drunken monkey: Removed outdated @todo comment.
46
- #2972510 by msankhala, drunken monkey: Replaced Unicode::* methods with mb_*
57
functions.

Diff for: modules/search_api_db/config/schema/search_api_db.backend.schema.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ plugin.plugin_configuration.search_api_backend.search_api_db:
88
min_chars:
99
type: 'integer'
1010
label: 'Minimum length of indexed words'
11-
partial_matches:
12-
type: 'boolean'
13-
label: 'Whether to also search on parts of a word'
11+
matching:
12+
type: 'string'
13+
label: 'The matching mode – "words", "prefix" or "partial"'
1414
autocomplete:
1515
type: mapping
1616
label: Autcomplete configuration

Diff for: modules/search_api_db/search_api_db.install

+31
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,34 @@ function search_api_db_update_8102() {
7676

7777
return t('Primary keys added to all denormalized index tables.');
7878
}
79+
80+
/**
81+
* Converts the old "partial_matches" option to the new "matching" option.
82+
*/
83+
function search_api_db_update_8103() {
84+
// @see https://www.drupal.org/project/search_api/issues/2971033
85+
86+
$config_factory = \Drupal::configFactory();
87+
$count = 0;
88+
foreach ($config_factory->listAll('search_api.server.') as $server_id) {
89+
$server = $config_factory->getEditable($server_id);
90+
if ($server->get('backend') !== 'search_api_db') {
91+
continue;
92+
}
93+
94+
++$count;
95+
$config = $server->get('backend_config') ?: [];
96+
$config['matching'] = empty($config['partial_matches']) ? 'words' : 'partial';
97+
unset($config['partial_matches']);
98+
$server->set('backend_config', $config);
99+
// Mark the resulting configuration as trusted data. This avoids issues
100+
// with future schema changes.
101+
$server->save(TRUE);
102+
}
103+
104+
if ($count) {
105+
return \Drupal::translation()
106+
->formatPlural($count, 'Updated 1 server.', 'Updated @count servers.');
107+
}
108+
return NULL;
109+
}

Diff for: modules/search_api_db/search_api_db_defaults/config/optional/search_api.server.default_server.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ backend: search_api_db
66
backend_config:
77
database: 'default:default'
88
min_chars: 3
9-
partial_matches: false
9+
matching: 'words'
1010
autocomplete:
1111
suggest_suffix: true
1212
suggest_words: true

Diff for: modules/search_api_db/src/Plugin/search_api/backend/Database.php

+27-13
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ public function defaultConfiguration() {
399399
return [
400400
'database' => NULL,
401401
'min_chars' => 1,
402-
'partial_matches' => FALSE,
402+
'matching' => 'words',
403403
'autocomplete' => [
404404
'suggest_suffix' => TRUE,
405405
'suggest_words' => TRUE,
@@ -465,11 +465,15 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
465465
'#default_value' => $this->configuration['min_chars'],
466466
];
467467

468-
$form['partial_matches'] = [
469-
'#type' => 'checkbox',
470-
'#title' => $this->t('Search on parts of a word'),
471-
'#description' => $this->t('Find keywords in parts of a word, too. (For example, find results with "database" when searching for "base"). <strong>Caution:</strong> This can make searches much slower on large sites!'),
472-
'#default_value' => $this->configuration['partial_matches'],
468+
$form['matching'] = [
469+
'#type' => 'radios',
470+
'#title' => $this->t('Partial matching'),
471+
'#default_value' => $this->configuration['matching'],
472+
'#options' => [
473+
'words' => $this->t('Match whole words only'),
474+
'partial' => $this->t('Match on parts of a word'),
475+
'prefix' => $this->t('Match words starting with given keywords'),
476+
],
473477
];
474478

475479
if ($this->getModuleHandler()->moduleExists('search_api_autocomplete')) {
@@ -511,10 +515,17 @@ public function viewSettings() {
511515
'info' => $this->configuration['min_chars'],
512516
];
513517
}
518+
519+
$labels = [
520+
'words' => $this->t('Match whole words only'),
521+
'partial' => $this->t('Match on parts of a word'),
522+
'prefix' => $this->t('Match words starting with given keywords'),
523+
];
514524
$info[] = [
515-
'label' => $this->t('Search on parts of a word'),
516-
'info' => !empty($this->configuration['partial_matches']) ? $this->t('enabled') : $this->t('disabled'),
525+
'label' => $this->t('Partial matching'),
526+
'info' => $labels[$this->configuration['matching']],
517527
];
528+
518529
if (!empty($this->configuration['autocomplete'])) {
519530
$this->configuration['autocomplete'] += [
520531
'suggest_suffix' => TRUE,
@@ -1887,8 +1898,9 @@ protected function createKeysQuery($keys, array $fields, array $all_fields, Inde
18871898
$db_query = NULL;
18881899
$mul_words = FALSE;
18891900
$neg_nested = $neg && $conj == 'AND';
1890-
$match_parts = !empty($this->configuration['partial_matches']);
1901+
$match_parts = $this->configuration['matching'] !== 'words';
18911902
$keyword_hits = [];
1903+
$prefix_search = $this->configuration['matching'] === 'prefix';
18921904

18931905
foreach ($keys as $i => $key) {
18941906
if (!Element::child($i)) {
@@ -1945,7 +1957,9 @@ protected function createKeysQuery($keys, array $fields, array $all_fields, Inde
19451957
}
19461958

19471959
foreach ($words as $i => $word) {
1948-
$db_or->condition('t.word', '%' . $this->database->escapeLike($word) . '%', 'LIKE');
1960+
$like = $this->database->escapeLike($word);
1961+
$like = $prefix_search ? "$like%" : "%$like%";
1962+
$db_or->condition('t.word', $like, 'LIKE');
19491963

19501964
// Add an expression for each keyword that shows whether the indexed
19511965
// word matches that particular keyword. That way we don't return a
@@ -2579,12 +2593,12 @@ public function getAutocompleteSuggestions(QueryInterface $query, SearchInterfac
25792593
$query->keys($user_input);
25802594
}
25812595
// To avoid suggesting incomplete words, we have to temporarily disable
2582-
// the "partial_matches" option. There should be no way we'll save the
2583-
// server during the createDbQuery() call, so this should be safe.
2596+
// partial matching. There should be no way we'll save the server during
2597+
// the createDbQuery() call, so this should be safe.
25842598
$configuration = $this->configuration;
25852599
$db_query = NULL;
25862600
try {
2587-
$this->configuration['partial_matches'] = FALSE;
2601+
$this->configuration['matching'] = 'words';
25882602
$db_query = $this->createDbQuery($query, $fields);
25892603
$this->configuration = $configuration;
25902604

Diff for: modules/search_api_db/tests/src/Kernel/BackendTest.php

+69-7
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ protected function checkBackendSpecificFeatures() {
7474
$this->checkMultiValuedInfo();
7575
$this->editServerPartial();
7676
$this->searchSuccessPartial();
77+
$this->editServerStartsWith();
78+
$this->searchSuccessStartsWith();
7779
$this->editServerMinChars();
7880
$this->searchSuccessMinChars();
7981
$this->checkUnknownOperator();
@@ -207,14 +209,11 @@ protected function checkMultiValuedInfo() {
207209

208210
/**
209211
* Edits the server to enable partial matches.
210-
*
211-
* @param bool $enable
212-
* (optional) Whether partial matching should be enabled or disabled.
213212
*/
214-
protected function editServerPartial($enable = TRUE) {
213+
protected function editServerPartial() {
215214
$server = $this->getServer();
216215
$backend_config = $server->getBackendConfig();
217-
$backend_config['partial_matches'] = $enable;
216+
$backend_config['matching'] = 'partial';
218217
$server->setBackendConfig($backend_config);
219218
$this->assertTrue((bool) $server->save(), 'The server was successfully edited.');
220219
$this->resetEntityCache();
@@ -243,7 +242,7 @@ protected function searchSuccessPartial() {
243242
$this->assertResults([], $results, 'Partial search for »foo nonexistent«');
244243

245244
$results = $this->buildSearch('bar nonexistent')->execute();
246-
$this->assertResults([], $results, 'Partial search for »foo nonexistent«');
245+
$this->assertResults([], $results, 'Partial search for »bar nonexistent«');
247246

248247
$keys = [
249248
'#conjunction' => 'AND',
@@ -271,14 +270,77 @@ protected function searchSuccessPartial() {
271270
$this->assertResults([1, 2, 3, 4], $results, 'Partial search with multi-field fulltext filter');
272271
}
273272

273+
/**
274+
* Edits the server to enable prefix matching.
275+
*/
276+
protected function editServerStartsWith() {
277+
$server = $this->getServer();
278+
$backend_config = $server->getBackendConfig();
279+
$backend_config['matching'] = 'prefix';
280+
$server->setBackendConfig($backend_config);
281+
$this->assertTrue((bool) $server->save(), 'The server was successfully edited.');
282+
$this->resetEntityCache();
283+
}
284+
285+
/**
286+
* Tests whether prefix matching works.
287+
*/
288+
protected function searchSuccessStartsWith() {
289+
$results = $this->buildSearch('foobaz')->range(0, 1)->execute();
290+
$this->assertResults([1], $results, 'Prefix search for »foobaz«');
291+
292+
$results = $this->buildSearch('foo', [], [], FALSE)
293+
->sort('search_api_relevance', QueryInterface::SORT_DESC)
294+
->sort('id')
295+
->execute();
296+
$this->assertResults([1, 2, 4, 3, 5], $results, 'Prefix search for »foo«');
297+
298+
$results = $this->buildSearch('foo tes')->execute();
299+
$this->assertResults([1, 2, 3, 4], $results, 'Prefix search for »foo tes«');
300+
301+
$results = $this->buildSearch('oob est')->execute();
302+
$this->assertResults([], $results, 'Prefix search for »oob est«');
303+
304+
$results = $this->buildSearch('foo nonexistent')->execute();
305+
$this->assertResults([], $results, 'Prefix search for »foo nonexistent«');
306+
307+
$results = $this->buildSearch('bar nonexistent')->execute();
308+
$this->assertResults([], $results, 'Prefix search for »bar nonexistent«');
309+
310+
$keys = [
311+
'#conjunction' => 'AND',
312+
'foob',
313+
[
314+
'#conjunction' => 'OR',
315+
'tes',
316+
'nonexistent',
317+
],
318+
];
319+
$results = $this->buildSearch($keys)->execute();
320+
$this->assertResults([1, 2, 3], $results, 'Prefix search for complex keys');
321+
322+
$results = $this->buildSearch('foo', ['category,item_category'], [], FALSE)
323+
->sort('id', QueryInterface::SORT_DESC)
324+
->execute();
325+
$this->assertResults([2, 1], $results, 'Prefix search for »foo« with additional filter');
326+
327+
$query = $this->buildSearch();
328+
$conditions = $query->createConditionGroup('OR');
329+
$conditions->addCondition('name', 'test');
330+
$conditions->addCondition('body', 'test');
331+
$query->addConditionGroup($conditions);
332+
$results = $query->execute();
333+
$this->assertResults([1, 2, 3, 4], $results, 'Prefix search with multi-field fulltext filter');
334+
}
335+
274336
/**
275337
* Edits the server to change the "Minimum word length" setting.
276338
*/
277339
protected function editServerMinChars() {
278340
$server = $this->getServer();
279341
$backend_config = $server->getBackendConfig();
280342
$backend_config['min_chars'] = 4;
281-
$backend_config['partial_matches'] = FALSE;
343+
$backend_config['matching'] = 'words';
282344
$server->setBackendConfig($backend_config);
283345
$success = (bool) $server->save();
284346
$this->assertTrue($success, 'The server was successfully edited.');

Diff for: tests/search_api_test_db/config/install/search_api.server.database_search_server.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ backend: search_api_db
55
backend_config:
66
database: 'default:default'
77
min_chars: 3
8-
partial_matches: false
8+
matching: words
99
status: true
1010
langcode: en
1111
dependencies:

0 commit comments

Comments
 (0)