diff --git a/components/Search/Controller/Search.php b/components/Search/Controller/Search.php index 3ef8574736..669dd7396a 100644 --- a/components/Search/Controller/Search.php +++ b/components/Search/Controller/Search.php @@ -31,19 +31,29 @@ class Search extends Controller { public function handle(Request $request) { - $q = $this->string('q'); - $criteria = Parser::parse($q); + $q = $this->string('q'); + [$note_criteria, $actor_criteria] = Parser::parse($q); - $qb = DB::createQueryBuilder(); - $qb->select('note')->from('App\Entity\Note', 'note'); - Event::handle('SeachQueryAddJoins', [&$qb]); - $qb->addCriteria($criteria); - $query = $qb->getQuery(); - $results = $query->execute(); + $note_qb = DB::createQueryBuilder(); + $actor_qb = DB::createQueryBuilder(); + $note_qb->select('note')->from('App\Entity\Note', 'note'); + $actor_qb->select('actor')->from('App\Entity\Actor', 'actor'); + Event::handle('SeachQueryAddJoins', [&$note_qb, &$actor_qb]); + $notes = $actors = []; + if (!is_null($note_criteria)) { + $note_qb->addCriteria($note_criteria); + $notes = $note_qb->getQuery()->execute(); + } else { + if (!is_null($actor_criteria)) { + $actor_qb->addCriteria($actor_criteria); + $actors = $actor_qb->getQuery()->execute(); + } + } return [ '_template' => 'search/show.html.twig', - 'results' => $results, + 'notes' => $notes, + 'actors' => $actors, ]; } } diff --git a/components/Search/Util/Parser.php b/components/Search/Util/Parser.php index ff31055781..96f91df967 100644 --- a/components/Search/Util/Parser.php +++ b/components/Search/Util/Parser.php @@ -34,29 +34,33 @@ abstract class Parser * recognises either spaces (currently `or`, should be fuzzy match), `OR` or `|` (`or`) and `AND` or `&` (`and`) * * TODO Better fuzzy match, implement exact match with quotes and nesting with parens + * + * @return Criteria[] */ - public static function parse(string $input, int $level = 0): Criteria + public static function parse(string $input, int $level = 0): array { if ($level === 0) { $input = trim(preg_replace(['/\s+/', '/\s+AND\s+/', '/\s+OR\s+/'], [' ', '&', '|'], $input), ' |&'); } - $left = $right = 0; - $lenght = mb_strlen($input); - $stack = []; - $eb = Criteria::expr(); - $criteria = []; - $parts = []; - $last_op = null; + $left = $right = 0; + $lenght = mb_strlen($input); + $stack = []; + $eb = Criteria::expr(); + $note_criteria_arr = []; + $actor_criteria_arr = []; + $note_parts = []; + $actor_parts = []; + $last_op = null; $connect_parts = /** - * Merge $parts into $criteria + * Merge $parts into $criteria_arr */ - function (bool $force = false) use ($eb, &$parts, $last_op, &$criteria) { + function (array &$parts, array &$criteria_arr, bool $force = false) use ($eb, $last_op) { foreach ([' ' => 'orX', '|' => 'orX', '&' => 'andX'] as $op => $func) { if ($last_op === $op || $force) { - $criteria[] = $eb->{$func}(...$parts); - $parts = []; + $criteria_arr[] = $eb->{$func}(...$parts); + $note_parts = []; break; } } @@ -68,18 +72,28 @@ abstract class Parser foreach (['&', '|', ' '] as $delimiter) { if ($input[$index] === $delimiter || $end = ($index === $lenght - 1)) { - $term = substr($input, $left, $end ? null : $right - $left); - $res = null; - $ret = Event::handle('SearchCreateExpression', [$eb, $term, &$res]); - if (is_null($res) || $ret == Event::next) { + $term = substr($input, $left, $end ? null : $right - $left); + $note_res = $actor_res = null; + $ret = Event::handle('SearchCreateExpression', [$eb, $term, &$note_res, &$actor_res]); + if ((is_null($note_res) && is_null($actor_res)) || $ret == Event::next) { throw new ServerException("No one claimed responsibility for a match term: {$term}"); + } else { + if (!is_null($note_res)) { + $note_parts[] = $note_res; + } else { + if (!is_null($actor_res)) { + $actor_parts[] = $actor_res; + } else { + throw new ServerException('Unexpected state in Search parser'); + } + } } - $parts[] = $res; $right = $left = $index + 1; if (!is_null($last_op) && $last_op !== $delimiter) { - $connect_parts(force: false); + $connect_parts($note_parts, $note_criteria_arr, force: false); + $connect_parts($actor_parts, $actor_criteria_arr, force: false); } else { $last_op = $delimiter; } @@ -92,10 +106,17 @@ abstract class Parser } } - if (!empty($parts)) { - $connect_parts(force: true); + $note_criteria = $actor_criteria = null; + if (!empty($note_parts)) { + $connect_parts($note_parts, $note_criteria_arr, force: true); + $note_criteria = new Criteria($eb->orX(...$note_criteria_arr)); + } else { + if (!empty($actor_parts)) { + $connect_parts($actor_parts, $actor_criteria_arr, force: true); + $actor_criteria = new Criteria($eb->orX(...$actor_criteria_arr)); + } } - return new Criteria($eb->orX(...$criteria)); + return [$note_criteria, $actor_criteria]; } } diff --git a/components/Search/templates/search/show.html.twig b/components/Search/templates/search/show.html.twig index a62a260fa5..94dc9952d1 100644 --- a/components/Search/templates/search/show.html.twig +++ b/components/Search/templates/search/show.html.twig @@ -1,7 +1,7 @@ -{% for res in results %} - {% if res is instanceof('App\\Entity\\Note') %} - {% include '/cards/note/view.html.twig' with {'note': res} %} - {% else %} - {{ dump(res) }} - {% endif %} +{% for note in notes %} + {% include '/cards/note/view.html.twig' with {'note': note} %} +{% endfor %} + +{% for actor in actors %} + {% include 'actor/view.html.twig' with {'actor': actor} %} {% endfor %} diff --git a/components/Tag/Tag.php b/components/Tag/Tag.php index 8bad61d58e..86ced208f8 100644 --- a/components/Tag/Tag.php +++ b/components/Tag/Tag.php @@ -90,18 +90,36 @@ class Tag extends Component return substr(Formatting::slugify($tag), 0, self::MAX_TAG_LENGTH); } - public function onSearchCreateExpression(ExpressionBuilder $eb, string $term, &$expr) + /** + * Populate $note_expr with an expression to match a tag, if the term looks like a tag + * + * $term /^(note|tag|people|actor)/ means we want to match only either a note or an actor + * + * @param mixed $note_expr + * @param mixed $actor_expr + */ + public function onSearchCreateExpression(ExpressionBuilder $eb, string $term, &$note_expr, &$actor_expr) { - if (preg_match(self::TAG_REGEX, $term)) { - $expr = $eb->eq('note_tag.tag', $term); - return Event::stop; + $search_term = str_contains($term, ':#') ? explode(':', $term)[1] : $term; + $temp_note_expr = $eb->eq('note_tag.tag', $search_term); + $temp_actor_expr = $eb->eq('actor_tag.tag', $search_term); + if (Formatting::startsWith($term, ['note', 'tag'])) { + $note_expr = $temp_note_expr; } else { - return Event::next; + if (Formatting::startsWith($term, ['people', 'actor'])) { + $actor_expr = $temp_actor_expr; + } else { + $note_expr = $temp_note_expr; + $actor_expr = $temp_actor_expr; + return Event::next; + } } + return Event::stop; } - public function onSeachQueryAddJoins(QueryBuilder &$qb) + public function onSeachQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb) { - $qb->join('App\Entity\NoteTag', 'note_tag', Expr\Join::WITH, 'note_tag.note_id = note.id'); + $note_qb->join('App\Entity\NoteTag', 'note_tag', Expr\Join::WITH, 'note_tag.note_id = note.id'); + $actor_qb->join('App\Entity\ActorTag', 'actor_tag', Expr\Join::WITH, 'actor_tag.tagger = actor.id'); } }