Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[core] SearchParameters as a class instead of an array, see #1153 #1268

Merged
merged 1 commit into from
Sep 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 49 additions & 55 deletions core/imageboard/search.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,39 @@ public function __construct(
}
}

class SearchParameters
{
/** @var TagCondition[] */
public array $tag_conditions = [];
/** @var ImgCondition[] */
public array $img_conditions = [];
public ?string $order = null;

/**
* Turn a human input string into a an abstract search query
*
* @param string[] $terms
*/
public static function from_terms(array $terms): SearchParameters
{
global $config;

$sp = new SearchParameters();

$stpen = 0; // search term parse event number
foreach (array_merge([null], $terms) as $term) {
$stpe = send_event(new SearchTermParseEvent($stpen++, $term, $terms));
$sp->order ??= $stpe->order;
$sp->img_conditions = array_merge($sp->img_conditions, $stpe->img_conditions);
$sp->tag_conditions = array_merge($sp->tag_conditions, $stpe->tag_conditions);
}

$sp->order ??= "images.".$config->get_string(IndexConfig::ORDER);

return $sp;
}
}

class Search
{
/**
Expand Down Expand Up @@ -100,8 +133,8 @@ private static function find_images_internal(int $start = 0, ?int $limit = null,
}
}

[$tag_conditions, $img_conditions, $order] = self::terms_to_conditions($tags);
$querylet = self::build_search_querylet($tag_conditions, $img_conditions, $order, $limit, $start);
$params = SearchParameters::from_terms($tags);
$querylet = self::build_search_querylet($params, $limit, $start);
return $database->get_all_iterable($querylet->sql, $querylet->variables);
}

Expand Down Expand Up @@ -206,8 +239,8 @@ public static function count_images(array $tags = []): int
$cache_key = "image-count:" . md5(Tag::implode($tags));
$total = $cache->get($cache_key);
if (is_null($total)) {
[$tag_conditions, $img_conditions, $order] = self::terms_to_conditions($tags);
$querylet = self::build_search_querylet($tag_conditions, $img_conditions, null);
$params = SearchParameters::from_terms($tags);
$querylet = self::build_search_querylet($params);
$total = (int)$database->get_one("SELECT COUNT(*) AS cnt FROM ($querylet->sql) AS tbl", $querylet->variables);
if ($speed_hax && $total > 5000) {
// when we have a ton of images, the count
Expand All @@ -233,58 +266,19 @@ private static function tag_or_wildcard_to_ids(string $tag): array
return $database->get_col($sq, ["tag" => Tag::sqlify($tag)]);
}

/**
* Turn a human input string into a an abstract search query
*
* (This is only public for testing purposes, nobody should be calling this
* directly from outside this class)
*
* @param string[] $terms
* @return array{0: TagCondition[], 1: ImgCondition[], 2: string}
*/
public static function terms_to_conditions(array $terms): array
{
global $config;

$tag_conditions = [];
$img_conditions = [];
$order = null;

/*
* Turn a bunch of strings into a bunch of TagCondition
* and ImgCondition objects
*/
$stpen = 0; // search term parse event number
foreach (array_merge([null], $terms) as $term) {
$stpe = send_event(new SearchTermParseEvent($stpen++, $term, $terms));
$order ??= $stpe->order;
$img_conditions = array_merge($img_conditions, $stpe->img_conditions);
$tag_conditions = array_merge($tag_conditions, $stpe->tag_conditions);
}

$order = ($order ?: "images.".$config->get_string(IndexConfig::ORDER));

return [$tag_conditions, $img_conditions, $order];
}

/**
* Turn an abstract search query into an SQL Querylet
*
* (This is only public for testing purposes, nobody should be calling this
* directly from outside this class)
*
* @param TagCondition[] $tag_conditions
* @param ImgCondition[] $img_conditions
*/
public static function build_search_querylet(
array $tag_conditions,
array $img_conditions,
?string $order = null,
SearchParameters $params,
?int $limit = null,
?int $offset = null
): Querylet {
// no tags, do a simple search
if (count($tag_conditions) === 0) {
if (count($params->tag_conditions) === 0) {
static::$_search_path[] = "no_tags";
$query = new Querylet("SELECT images.* FROM images WHERE 1=1");
}
Expand All @@ -293,24 +287,24 @@ public static function build_search_querylet(
// and do the offset / limit there, which is 10x faster than fetching
// all the image_tags and doing the offset / limit on the result.
elseif (
count($tag_conditions) === 1
&& $tag_conditions[0]->positive
count($params->tag_conditions) === 1
&& $params->tag_conditions[0]->positive
// We can only do this if img_conditions is empty, because
// we're going to apply the offset / limit to the image_tags
// subquery, and applying extra conditions to the top-level
// query might reduce the total results below the target limit
&& empty($img_conditions)
&& empty($params->img_conditions)
// We can only do this if we're sorting by ID, because
// we're going to be using the image_tags table, which
// only has image_id and tag_id, not any other columns
&& ($order == "id DESC" || $order == "images.id DESC")
&& ($params->order == "id DESC" || $params->order == "images.id DESC")
// This is only an optimisation if we are applying limit
// and offset
&& !is_null($limit)
&& !is_null($offset)
) {
static::$_search_path[] = "fast";
$tc = $tag_conditions[0];
$tc = $params->tag_conditions[0];
// IN (SELECT id FROM tags) is 100x slower than doing a separate
// query and then a second query for IN(first_query_results)??
$tag_array = self::tag_or_wildcard_to_ids($tc->tag);
Expand Down Expand Up @@ -346,7 +340,7 @@ public static function build_search_querylet(
$negative_tag_id_array = [];
$all_nonexistent_negatives = true;

foreach ($tag_conditions as $tq) {
foreach ($params->tag_conditions as $tq) {
$tag_ids = self::tag_or_wildcard_to_ids($tq->tag);
$tag_count = count($tag_ids);

Expand Down Expand Up @@ -435,11 +429,11 @@ public static function build_search_querylet(
* Merge all the image metadata searches into one generic querylet
* and append to the base querylet with "AND blah"
*/
if (!empty($img_conditions)) {
if (!empty($params->img_conditions)) {
$n = 0;
$img_sql = "";
$img_vars = [];
foreach ($img_conditions as $iq) {
foreach ($params->img_conditions as $iq) {
if ($n++ > 0) {
$img_sql .= " AND";
}
Expand All @@ -453,8 +447,8 @@ public static function build_search_querylet(
$query->append(new Querylet($img_sql, $img_vars));
}

if (!is_null($order)) {
$query->append(new Querylet(" ORDER BY ".$order));
if (!is_null($params->order)) {
$query->append(new Querylet(" ORDER BY ".$params->order));
}

if (!is_null($limit)) {
Expand Down
114 changes: 114 additions & 0 deletions core/tests/SearchParametersTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php

declare(strict_types=1);

namespace Shimmie2;

use PHPUnit\Framework\Attributes\Depends;
use PHPUnit\Framework\Constraint\IsEqual;

require_once "core/imageboard/search.php";

class SearchParametersTest extends ShimmiePHPUnitTestCase
{
/**
* @param string $tags
* @param TagCondition[] $expected_tag_conditions
* @param ImgCondition[] $expected_img_conditions
* @param string $expected_order
*/
private function assert_TTC(
string $tags,
array $expected_tag_conditions,
array $expected_img_conditions,
string $expected_order,
): void {
$params = SearchParameters::from_terms(Tag::explode($tags, false));

static::assertThat(
[
"tags" => $expected_tag_conditions,
"imgs" => $expected_img_conditions,
"order" => $expected_order,
],
new IsEqual([
"tags" => $params->tag_conditions,
"imgs" => $params->img_conditions,
"order" => $params->order,
])
);
}

public function testTTC_Empty(): void
{
$this->assert_TTC(
"",
[
],
[
new ImgCondition(new Querylet("trash != :true", ["true" => true])),
new ImgCondition(new Querylet("private != :true OR owner_id = :private_owner_id", [
"private_owner_id" => 1,
"true" => true])),
new ImgCondition(new Querylet("rating IN ('?', 's', 'q', 'e')", [])),
],
"images.id DESC"
);
}

public function testTTC_Hash(): void
{
$this->assert_TTC(
"hash=1234567890",
[
],
[
new ImgCondition(new Querylet("trash != :true", ["true" => true])),
new ImgCondition(new Querylet("private != :true OR owner_id = :private_owner_id", [
"private_owner_id" => 1,
"true" => true])),
new ImgCondition(new Querylet("rating IN ('?', 's', 'q', 'e')", [])),
new ImgCondition(new Querylet("images.hash = :hash", ["hash" => "1234567890"])),
],
"images.id DESC"
);
}

public function testTTC_Ratio(): void
{
$this->assert_TTC(
"ratio=42:12345",
[
],
[
new ImgCondition(new Querylet("trash != :true", ["true" => true])),
new ImgCondition(new Querylet("private != :true OR owner_id = :private_owner_id", [
"private_owner_id" => 1,
"true" => true])),
new ImgCondition(new Querylet("rating IN ('?', 's', 'q', 'e')", [])),
new ImgCondition(new Querylet("width / :width1 = height / :height1", ['width1' => 42,
'height1' => 12345])),
],
"images.id DESC"
);
}

public function testTTC_Order(): void
{
$this->assert_TTC(
"order=score",
[
],
[
new ImgCondition(new Querylet("trash != :true", ["true" => true])),
new ImgCondition(new Querylet("private != :true OR owner_id = :private_owner_id", [
"private_owner_id" => 1,
"true" => true])),
new ImgCondition(new Querylet("rating IN ('?', 's', 'q', 'e')", [])),
],
"images.numeric_score DESC"
);
}


}
Loading