Skip to content

Commit

Permalink
Added QueryBuilder
Browse files Browse the repository at this point in the history
Added limit search term
Fixed tag count evaluation for single-tag searches
  • Loading branch information
sanmadjack committed Jun 1, 2024
1 parent 5cbbe5e commit 6edfb9f
Show file tree
Hide file tree
Showing 8 changed files with 1,091 additions and 140 deletions.
328 changes: 222 additions & 106 deletions core/imageboard/search.php

Large diffs are not rendered by default.

754 changes: 754 additions & 0 deletions core/query_builder.php

Large diffs are not rendered by default.

56 changes: 42 additions & 14 deletions core/tests/SearchTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,31 +72,36 @@ public function testUpload(): array
* @param string $tags
* @param TagCondition[] $expected_tag_conditions
* @param ImgCondition[] $expected_img_conditions
* @param string $expected_order
* @param mixed[] $expected_order
* @param ?int $expected_limit
*/
private function assert_TTC(
string $tags,
array $expected_tag_conditions,
array $expected_img_conditions,
string $expected_order,
array $expected_order,
?int $expected_limit
): void {
$class = new \ReflectionClass(Search::class);
$terms_to_conditions = $class->getMethod("terms_to_conditions");
$terms_to_conditions->setAccessible(true); // Use this if you are running PHP older than 8.1.0

$obj = new Search();
[$tag_conditions, $img_conditions, $order] = $terms_to_conditions->invokeArgs($obj, [Tag::explode($tags, false)]);
/** @var TermConditions $conditions */
$conditions = $terms_to_conditions->invokeArgs($obj, [Tag::explode($tags, false)]);

static::assertThat(
[
"tags" => $expected_tag_conditions,
"imgs" => $expected_img_conditions,
"order" => $expected_order,
"limit" => $expected_limit,
],
new IsEqual([
"tags" => $tag_conditions,
"imgs" => $img_conditions,
"order" => $order,
"tags" => $conditions->tag_conditions,
"imgs" => $conditions->img_conditions,
"order" => $conditions->order,
"limit" => $conditions->limit,
])
);
}
Expand All @@ -114,7 +119,8 @@ public function testTTC_Empty(): void
"true" => true])),
new ImgCondition(new Querylet("rating IN ('?', 's', 'q', 'e')", [])),
],
"images.id DESC"
["images.id DESC"],
null
);
}

Expand All @@ -132,7 +138,8 @@ public function testTTC_Hash(): void
new ImgCondition(new Querylet("rating IN ('?', 's', 'q', 'e')", [])),
new ImgCondition(new Querylet("images.hash = :hash", ["hash" => "1234567890"])),
],
"images.id DESC"
["images.id DESC"],
null
);
}

Expand All @@ -151,10 +158,28 @@ public function testTTC_Ratio(): void
new ImgCondition(new Querylet("width / :width1 = height / :height1", ['width1' => 42,
'height1' => 12345])),
],
"images.id DESC"
["images.id DESC"],
null
);
}

public function testTTC_Limit(): void
{
$this->assert_TTC(
"limit=12",
[
],
[
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"],
12
);
}
public function testTTC_Order(): void
{
$this->assert_TTC(
Expand All @@ -168,7 +193,8 @@ public function testTTC_Order(): void
"true" => true])),
new ImgCondition(new Querylet("rating IN ('?', 's', 'q', 'e')", [])),
],
"images.numeric_score DESC"
["images.numeric_score DESC"],
null
);
}

Expand Down Expand Up @@ -210,13 +236,15 @@ private function assert_BSQ(
Search::$_search_path = [];

$class = new \ReflectionClass(Search::class);
$build_search_querylet = $class->getMethod("build_search_querylet");
$build_search_querylet->setAccessible(true); // Use this if you are running PHP older than 8.1.0
$build_search_query = $class->getMethod("build_search_query");
$build_search_query->setAccessible(true); // Use this if you are running PHP older than 8.1.0

$obj = new Search();
$querylet = $build_search_querylet->invokeArgs($obj, [$tcs, $ics, $order, $limit, $start]);
/** @var QueryBuilder $query_builder */
$query_builder = $build_search_query->invokeArgs($obj, [$tcs, $ics, $order, $limit, $start]);
$query = $query_builder->render();

$results = $database->get_all($querylet->sql, $querylet->variables);
$results = $database->get_all($query->sql, $query->parameters);

static::assertThat(
[
Expand Down
27 changes: 27 additions & 0 deletions core/util.php
Original file line number Diff line number Diff line change
Expand Up @@ -812,3 +812,30 @@ function shm_tempnam(string $prefix = ""): string
$temp = \Safe\realpath("data/temp");
return \Safe\tempnam($temp, $prefix);
}

// Acquired from https://stackoverflow.com/questions/139474/how-can-i-capture-the-result-of-var-dump-to-a-string
function return_var_dump(mixed...$args): string
{
ob_start();
try {
var_dump(...$args);
$output = ob_get_clean();
if($output === false) {
return "";
}
return $output;
} catch (\Throwable $ex) {
// PHP8 ArgumentCountError for 0 arguments, probably..
// in php<8 this was just a warning
ob_end_clean();
throw $ex;
}
}

function var_dump_format(mixed $input, ?string $title = null): void
{
if(!empty($title)) {
echo "<h2>$title</h2>";
}
echo "<pre>".html_escape(return_var_dump($input))."</pre><br/>";
}
3 changes: 2 additions & 1 deletion ext/index/events.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ class SearchTermParseEvent extends Event
public array $img_conditions = [];
/** @var TagCondition[] */
public array $tag_conditions = [];
public ?string $order = null;
public null|string|QueryBuilderOrder $order = null;
public ?int $limit = null;

/**
* @param string[] $context
Expand Down
30 changes: 16 additions & 14 deletions ext/index/main.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Shimmie2;

use _PHPStan_39fe102d2\Nette\Neon\Exception;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\{InputInterface,InputArgument};
use Symfony\Component\Console\Input\InputOption;
Expand Down Expand Up @@ -87,7 +88,7 @@ public function onPageRequest(PageRequestEvent $event): void
}
}
if (is_null($images)) {
$images = Search::find_images(($page_number - 1) * $page_size, $page_size, $search_terms);
$images = Search::find_images(offset: ($page_number - 1) * $page_size, limit: $page_size, tags: $search_terms);
}

$count_images = count($images);
Expand Down Expand Up @@ -164,24 +165,22 @@ public function onCliGen(CliGenEvent $event): void
$limit = $input->getOption('limit');
$count = $input->getOption('count');

[$tag_conditions, $img_conditions, $order] = Search::terms_to_conditions($search);
if($count) {
$order = null;
$page = null;
$limit = null;
}

$q = Search::build_search_querylet(
$tag_conditions,
$img_conditions,
$order,
$queryBuilder = Search::build_search_query_from_terms(
$search,
$limit,
(int)(($page - 1) * $limit),
(int)(($page - 1) * $limit)
);

if($count) {
$q = $queryBuilder->renderForCount();
} else {
$q = $queryBuilder->render();
}

$sql_str = $q->sql;
$sql_str = preg_replace("/\s+/", " ", $sql_str);
foreach($q->variables as $key => $val) {
foreach($q->parameters as $key => $val) {
if(is_string($val)) {
$sql_str = str_replace(":$key", "'$val'", $sql_str);
} else {
Expand Down Expand Up @@ -245,10 +244,13 @@ public function onSearchTermParse(SearchTermParseEvent $event): void
// recommended to change homepage to "post/list/order:dailyshuffle/1"
$seed = (int)date("Ymd");
$event->order = $database->seeded_random($seed, "images.id");
} elseif (preg_match("/^limit[=:](\d+)$/i", $event->term, $matches)) {
$limit = intval($matches[1]);
$event->limit = $limit;
}

// If we've reached this far, and nobody else has done anything with this term, then treat it as a tag
if ($event->order === null && $event->img_conditions == [] && $event->tag_conditions == []) {
if ($event->order === null && $event->limit === null && $event->img_conditions == [] && $event->tag_conditions == []) {
$event->add_tag_condition(new TagCondition($event->term, $event->positive));
}
}
Expand Down
6 changes: 6 additions & 0 deletions ext/index/theme.php
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,12 @@ public function get_help_html(): HTMLElement
P("Direction can be either asc or desc, indicating ascending (123) or descending (321) order."),
SHM_COMMAND_EXAMPLE("order:id_asc", "Returns posts sorted by ID, smallest first."),
SHM_COMMAND_EXAMPLE("order:width_desc", "Returns posts sorted by width, largest first."),
//
//
//
HR(),
H3("Limit the number of search results"),
SHM_COMMAND_EXAMPLE("limit=100", "Only returns the first 100 results"),
);
}
}
27 changes: 22 additions & 5 deletions ext/numeric_score/main.php
Original file line number Diff line number Diff line change
Expand Up @@ -199,18 +199,36 @@ public function onPageRequest(PageRequestEvent $event): void

$totaldate = $year."/".$month."/".$day;

$sql = "SELECT id FROM images WHERE EXTRACT(YEAR FROM posted) = :year";
if($database->get_driver_id() === DatabaseDriverID::SQLITE) {
$sql = "SELECT id FROM images WHERE strftime('%Y', posted) = cast(:year as text)";
$month = str_pad(strval($month), 2, "0", STR_PAD_LEFT);
$day = str_pad(strval($day), 2, "0", STR_PAD_LEFT);
} else {
$sql = "SELECT id FROM images WHERE EXTRACT(YEAR FROM posted) = :year";
}
$args = ["limit" => $config->get_int(IndexConfig::IMAGES), "year" => $year];

if ($event->page_matches("popular_by_day")) {
$sql .= " AND EXTRACT(MONTH FROM posted) = :month AND EXTRACT(DAY FROM posted) = :day";
if($database->get_driver_id() === DatabaseDriverID::SQLITE) {
$sql .= " AND strftime('%m', posted) = cast(:month as text) AND strftime('%d', posted) = cast(:day as text)";
} else {
$sql .= " AND EXTRACT(MONTH FROM posted) = :month AND EXTRACT(DAY FROM posted) = :day";
}
$args = array_merge($args, ["month" => $month, "day" => $day]);
$current = date("F jS, Y", \Safe\strtotime($totaldate)).
$current = date("F jS, Y", \Safe\strtotime($totaldate));
$name = "day";
$fmt = "\\y\\e\\a\\r\\=Y\\&\\m\\o\\n\\t\\h\\=m\\&\\d\\a\\y\\=d";
} elseif ($event->page_matches("popular_by_month")) {
$sql .= " AND EXTRACT(MONTH FROM posted) = :month";
if($database->get_driver_id() === DatabaseDriverID::SQLITE) {
$sql .= " AND strftime('%m', posted) = cast(:month as text)";
} else {
$sql .= " AND EXTRACT(MONTH FROM posted) = :month";
}
$args = array_merge($args, ["month" => $month]);
// PHP's -1 month and +1 month functionality break when modifying dates that are on the 31st of the month.
// See Example #3 on https://www.php.net/manual/en/datetime.modify.php
// To get around this, set the day to 1 when doing month work.
$totaldate = $year."/".$month."/01";
$current = date("F Y", \Safe\strtotime($totaldate));
$name = "month";
$fmt = "\\y\\e\\a\\r\\=Y\\&\\m\\o\\n\\t\\h\\=m";
Expand All @@ -225,7 +243,6 @@ public function onPageRequest(PageRequestEvent $event): void
$sql .= " AND NOT numeric_score=0 ORDER BY numeric_score DESC LIMIT :limit OFFSET 0";

//filter images by score != 0 + date > limit to max images on one page > order from highest to lowest score

$ids = $database->get_col($sql, $args);
$images = Search::get_images($ids);
$this->theme->view_popular($images, $totaldate, $current, $name, $fmt);
Expand Down

0 comments on commit 6edfb9f

Please sign in to comment.