mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-29 00:01:21 +00:00
Further improve returned search results
This commit is contained in:
@@ -51,6 +51,13 @@ class ElasticSearchDriver implements SearchDriverInterface
|
||||
|
||||
private const AUTOCOMPLETE_MIN_LENGTH = 2;
|
||||
|
||||
/**
|
||||
* Keep release text searches scoped to release title variants only.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
private const RELEASE_TEXT_FIELDS = ['searchname^3', 'plainsearchname^2'];
|
||||
|
||||
private static ?Client $client = null;
|
||||
|
||||
private static ?bool $availabilityCache = null;
|
||||
@@ -844,7 +851,7 @@ class ElasticSearchDriver implements SearchDriverInterface
|
||||
$search = $this->buildSearchQuery(
|
||||
index: $this->getReleasesIndex(),
|
||||
keywords: $keywords,
|
||||
fields: ['searchname^2', 'plainsearchname^1.5', 'fromname', 'filename', 'name^1.2'], // @phpstan-ignore argument.type
|
||||
fields: self::RELEASE_TEXT_FIELDS, // @phpstan-ignore argument.type
|
||||
limit: $limit
|
||||
);
|
||||
|
||||
@@ -891,7 +898,7 @@ class ElasticSearchDriver implements SearchDriverInterface
|
||||
$search = $this->buildSearchQuery(
|
||||
index: $this->getReleasesIndex(),
|
||||
keywords: $keywords,
|
||||
fields: ['searchname^2', 'plainsearchname^1.5', 'fromname', 'filename', 'name^1.2'], // @phpstan-ignore argument.type
|
||||
fields: self::RELEASE_TEXT_FIELDS, // @phpstan-ignore argument.type
|
||||
limit: $limit
|
||||
);
|
||||
|
||||
@@ -938,7 +945,7 @@ class ElasticSearchDriver implements SearchDriverInterface
|
||||
$search = $this->buildSearchQuery(
|
||||
index: $this->getReleasesIndex(),
|
||||
keywords: $keywords,
|
||||
fields: ['searchname^2', 'plainsearchname^1.5'], // @phpstan-ignore argument.type
|
||||
fields: self::RELEASE_TEXT_FIELDS, // @phpstan-ignore argument.type
|
||||
limit: $limit,
|
||||
options: [
|
||||
'boost' => 1.2,
|
||||
@@ -2792,8 +2799,9 @@ class ElasticSearchDriver implements SearchDriverInterface
|
||||
[
|
||||
'multi_match' => [
|
||||
'query' => $searchTerm,
|
||||
'fields' => ['searchname^3', 'name^2', 'filename'],
|
||||
'type' => 'best_fields',
|
||||
'fields' => self::RELEASE_TEXT_FIELDS,
|
||||
'type' => 'cross_fields',
|
||||
'operator' => 'and',
|
||||
'fuzziness' => 'AUTO',
|
||||
],
|
||||
],
|
||||
@@ -2862,24 +2870,46 @@ class ElasticSearchDriver implements SearchDriverInterface
|
||||
$must = [];
|
||||
|
||||
if ($hasText) {
|
||||
$text = is_string($phrases)
|
||||
? $phrases
|
||||
: implode(' ', array_values(array_filter(
|
||||
if (is_string($phrases)) {
|
||||
$text = trim($phrases);
|
||||
if ($text === '') {
|
||||
return ['ids' => [], 'total' => 0, 'fuzzy' => $useFuzzy];
|
||||
}
|
||||
$multi = [
|
||||
'query' => $text,
|
||||
'fields' => self::RELEASE_TEXT_FIELDS,
|
||||
'type' => 'cross_fields',
|
||||
'operator' => 'and',
|
||||
];
|
||||
if ($useFuzzy && $this->isFuzzyEnabled()) {
|
||||
$multi['fuzziness'] = $this->getFuzzyConfig()['fuzziness'] ?? 'AUTO';
|
||||
}
|
||||
$must[] = ['multi_match' => $multi];
|
||||
} elseif (is_array($phrases) && $this->isAssociativeStringArray($phrases)) {
|
||||
$must = $this->buildReleaseFieldSpecificMustClauses($phrases, $useFuzzy);
|
||||
if ($must === []) {
|
||||
return ['ids' => [], 'total' => 0, 'fuzzy' => $useFuzzy];
|
||||
}
|
||||
} else {
|
||||
$text = implode(' ', array_values(array_filter(
|
||||
is_array($phrases) ? $phrases : [],
|
||||
static fn ($v): bool => $v !== null && $v !== '' && $v !== -1
|
||||
)));
|
||||
if ($text === '') {
|
||||
return ['ids' => [], 'total' => 0, 'fuzzy' => $useFuzzy];
|
||||
$text = trim($text);
|
||||
if ($text === '') {
|
||||
return ['ids' => [], 'total' => 0, 'fuzzy' => $useFuzzy];
|
||||
}
|
||||
$multi = [
|
||||
'query' => $text,
|
||||
'fields' => self::RELEASE_TEXT_FIELDS,
|
||||
'type' => 'cross_fields',
|
||||
'operator' => 'and',
|
||||
];
|
||||
if ($useFuzzy && $this->isFuzzyEnabled()) {
|
||||
$multi['fuzziness'] = $this->getFuzzyConfig()['fuzziness'] ?? 'AUTO';
|
||||
}
|
||||
$must[] = ['multi_match' => $multi];
|
||||
}
|
||||
$multi = [
|
||||
'query' => $text,
|
||||
'fields' => ['searchname^3', 'name^2', 'filename', 'plainsearchname'],
|
||||
'type' => 'best_fields',
|
||||
];
|
||||
if ($useFuzzy && $this->isFuzzyEnabled()) {
|
||||
$multi['fuzziness'] = $this->getFuzzyConfig()['fuzziness'] ?? 'AUTO';
|
||||
}
|
||||
$must[] = ['multi_match' => $multi];
|
||||
}
|
||||
|
||||
if ($must !== [] && $filter !== []) {
|
||||
@@ -2946,6 +2976,69 @@ class ElasticSearchDriver implements SearchDriverInterface
|
||||
return $run(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $phrases
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
private function buildReleaseFieldSpecificMustClauses(array $phrases, bool $useFuzzy): array
|
||||
{
|
||||
$fieldMap = [
|
||||
'searchname' => self::RELEASE_TEXT_FIELDS,
|
||||
'plainsearchname' => ['plainsearchname'],
|
||||
'name' => ['name'],
|
||||
'fromname' => ['fromname'],
|
||||
'filename' => ['filename'],
|
||||
];
|
||||
|
||||
$must = [];
|
||||
foreach ($phrases as $field => $rawValue) {
|
||||
if (! is_string($field) || ! isset($fieldMap[$field])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = is_scalar($rawValue) ? trim((string) $rawValue) : '';
|
||||
if ($value === '' || $value === '-1') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fields = $fieldMap[$field];
|
||||
if (count($fields) > 1) {
|
||||
$multi = [
|
||||
'query' => $value,
|
||||
'fields' => $fields,
|
||||
'type' => 'cross_fields',
|
||||
'operator' => 'and',
|
||||
];
|
||||
if ($useFuzzy && $this->isFuzzyEnabled()) {
|
||||
$multi['fuzziness'] = $this->getFuzzyConfig()['fuzziness'] ?? 'AUTO';
|
||||
}
|
||||
$must[] = ['multi_match' => $multi];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$fieldName = $fields[0];
|
||||
$match = [
|
||||
'query' => $value,
|
||||
'operator' => 'and',
|
||||
];
|
||||
if ($useFuzzy && $this->isFuzzyEnabled()) {
|
||||
$match['fuzziness'] = $this->getFuzzyConfig()['fuzziness'] ?? 'AUTO';
|
||||
}
|
||||
$must[] = ['match' => [$fieldName => $match]];
|
||||
}
|
||||
|
||||
return $must;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string|int, mixed> $phrases
|
||||
*/
|
||||
private function isAssociativeStringArray(array $phrases): bool
|
||||
{
|
||||
return count(array_filter(array_keys($phrases), 'is_string')) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
|
||||
@@ -2349,7 +2349,7 @@ class ManticoreSearchDriver implements SearchDriverInterface
|
||||
return $this->searchReleasesByCategory($categoryIds, $limit);
|
||||
}
|
||||
|
||||
$searchExpr = '@@relaxed @searchname '.$preparedSearch;
|
||||
$searchExpr = '@@relaxed '.self::scopePreparedQueryToField($preparedSearch, '@searchname');
|
||||
|
||||
$query = (new Search($this->manticoreSearch))
|
||||
->setTable($this->getReleasesIndex())
|
||||
@@ -2428,7 +2428,7 @@ class ManticoreSearchDriver implements SearchDriverInterface
|
||||
}
|
||||
$prepared = self::prepareUserSearchQuery((string) $value);
|
||||
if ($prepared !== '') {
|
||||
$terms[] = '@@relaxed @'.$key.' '.$prepared;
|
||||
$terms[] = '@@relaxed '.self::scopePreparedQueryToField($prepared, '@'.$key);
|
||||
}
|
||||
}
|
||||
if ($terms === []) {
|
||||
|
||||
@@ -135,6 +135,43 @@ final class MovieSearchApiTest extends TestCase
|
||||
$this->assertNotContains('Titanic.The.Digital.Resurrection.2025.720p.WEBRip-LAMA', $results->pluck('searchname')->all());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function api_search_uses_search_releases_filtered_with_search_phrase(): void
|
||||
{
|
||||
$mock = Mockery::mock(SearchService::class, [$this->app]);
|
||||
$mock->shouldReceive('isAvailable')->andReturn(true);
|
||||
$mock->shouldReceive('searchReleasesFiltered')
|
||||
->once()
|
||||
->withArgs(function (array $criteria, int $limit, int $offset): bool {
|
||||
return ($criteria['phrases'] ?? null) === 'Resurrection 2025'
|
||||
&& ($criteria['try_fuzzy'] ?? null) === true
|
||||
&& $limit === 100
|
||||
&& $offset === 0;
|
||||
})
|
||||
->andReturn([
|
||||
'ids' => [],
|
||||
'total' => 0,
|
||||
'fuzzy' => false,
|
||||
]);
|
||||
|
||||
$this->app->instance(SearchService::class, $mock);
|
||||
|
||||
$service = new ReleaseSearchService;
|
||||
$results = $service->apiSearch(
|
||||
'Resurrection 2025',
|
||||
-1,
|
||||
0,
|
||||
100,
|
||||
-1,
|
||||
[],
|
||||
[2030],
|
||||
0,
|
||||
'posted_desc'
|
||||
);
|
||||
|
||||
$this->assertCount(0, $results);
|
||||
}
|
||||
|
||||
private function registerSqliteConcatIfNeeded(): void
|
||||
{
|
||||
if (DB::getDriverName() !== 'sqlite') {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class ElasticSearchQueryTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function it_uses_release_text_fields_constant_with_searchname_variants_only(): void
|
||||
{
|
||||
$driverSource = file_get_contents(__DIR__.'/../../app/Services/Search/Drivers/ElasticSearchDriver.php');
|
||||
|
||||
$this->assertIsString($driverSource);
|
||||
$this->assertStringContainsString(
|
||||
"private const RELEASE_TEXT_FIELDS = ['searchname^3', 'plainsearchname^2'];",
|
||||
$driverSource
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function search_releases_filtered_uses_cross_fields_and_operator_and_for_release_text_queries(): void
|
||||
{
|
||||
$driverSource = file_get_contents(__DIR__.'/../../app/Services/Search/Drivers/ElasticSearchDriver.php');
|
||||
|
||||
$this->assertIsString($driverSource);
|
||||
$this->assertStringContainsString("'fields' => self::RELEASE_TEXT_FIELDS", $driverSource);
|
||||
$this->assertStringContainsString("'type' => 'cross_fields'", $driverSource);
|
||||
$this->assertStringContainsString("'operator' => 'and'", $driverSource);
|
||||
$this->assertStringNotContainsString(
|
||||
"'fields' => ['searchname^3', 'name^2', 'filename', 'plainsearchname']",
|
||||
$driverSource
|
||||
);
|
||||
$this->assertStringNotContainsString("'type' => 'best_fields'", $driverSource);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function releases_index_search_entrypoints_use_release_text_fields_constant(): void
|
||||
{
|
||||
$driverSource = file_get_contents(__DIR__.'/../../app/Services/Search/Drivers/ElasticSearchDriver.php');
|
||||
|
||||
$this->assertIsString($driverSource);
|
||||
$this->assertGreaterThanOrEqual(
|
||||
3,
|
||||
substr_count($driverSource, 'fields: self::RELEASE_TEXT_FIELDS'),
|
||||
'Expected indexSearch/indexSearchApi/indexSearchTMA to use RELEASE_TEXT_FIELDS.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -237,6 +237,38 @@ class ManticoreSearchQueryTest extends TestCase
|
||||
$this->assertStringContainsString("\$query->sort('id', \$order);", $driverSource);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function search_releases_filtered_scopes_field_queries_with_grouping_parentheses(): void
|
||||
{
|
||||
$driverSource = file_get_contents(__DIR__.'/../../app/Services/Search/Drivers/ManticoreSearchDriver.php');
|
||||
|
||||
$this->assertIsString($driverSource);
|
||||
$this->assertStringContainsString(
|
||||
"\$terms[] = '@@relaxed '.self::scopePreparedQueryToField(\$prepared, '@'.\$key);",
|
||||
$driverSource
|
||||
);
|
||||
$this->assertStringNotContainsString(
|
||||
"\$terms[] = '@@relaxed @'.\$key.' '.\$prepared;",
|
||||
$driverSource
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function search_releases_with_category_filter_scopes_searchname_with_grouping_parentheses(): void
|
||||
{
|
||||
$driverSource = file_get_contents(__DIR__.'/../../app/Services/Search/Drivers/ManticoreSearchDriver.php');
|
||||
|
||||
$this->assertIsString($driverSource);
|
||||
$this->assertStringContainsString(
|
||||
"\$searchExpr = '@@relaxed '.self::scopePreparedQueryToField(\$preparedSearch, '@searchname');",
|
||||
$driverSource
|
||||
);
|
||||
$this->assertStringNotContainsString(
|
||||
"\$searchExpr = '@@relaxed @searchname '.\$preparedSearch;",
|
||||
$driverSource
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function search_releases_filtered_uses_stable_id_tiebreak_sort_for_elasticsearch(): void
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user