*/ class ContentDocumentRepository extends ServiceEntityRepository { public function __construct(ManagerRegistry $registry) { parent::__construct($registry, ContentDocument::class); } /** * @return array keyed by ContentDocument::getKey() */ public function findAllByKey(): array { $documents = []; foreach ($this->findAll() as $document) { $documents[$document->getKey()] = $document; } return $documents; } /** * Full-text search over the indexed documents that can be linked to. * * DBAL rather than DQL: Doctrine has no MATCH ... AGAINST. The score is weighted by type * so that, at similar relevance, what people book ranks above what they read. * * @param string $booleanQuery a MySQL boolean-mode query, built by ContentSearchQuery * @param list $types empty for all * @param array $typeWeights * * @return list */ public function searchFulltext(string $booleanQuery, array $types, ?string $site, array $typeWeights, int $limit): array { $weight = 'CASE type'; $params = ['query' => $booleanQuery, 'limit' => $limit]; $paramTypes = ['limit' => ParameterType::INTEGER]; foreach (array_keys($typeWeights) as $i => $type) { $weight .= sprintf(' WHEN :wt%d THEN :wv%d', $i, $i); $params['wt'.$i] = $type; $params['wv'.$i] = $typeWeights[$type]; } $weight .= ' ELSE 1 END'; $sql = 'SELECT type, uid, title, url, site, breadcrumb, body_md,' .' MATCH(title, breadcrumb, body_md) AGAINST(:query IN BOOLEAN MODE) * '.$weight.' AS score' .' FROM content_document' .' WHERE indexed = 1 AND url IS NOT NULL' .' AND MATCH(title, breadcrumb, body_md) AGAINST(:query IN BOOLEAN MODE)'; if ([] !== $types) { $sql .= ' AND type IN (:types)'; $params['types'] = $types; $paramTypes['types'] = ArrayParameterType::STRING; } if (null !== $site) { $sql .= ' AND site = :site'; $params['site'] = $site; } $sql .= ' ORDER BY score DESC, uid ASC LIMIT :limit'; $rows = $this->getEntityManager()->getConnection()->fetchAllAssociative($sql, $params, $paramTypes); return array_map(static fn (array $row): array => [ 'type' => (string) $row['type'], 'uid' => (int) $row['uid'], 'title' => (string) $row['title'], 'url' => (string) $row['url'], 'site' => null === $row['site'] ? null : (string) $row['site'], 'breadcrumb' => null === $row['breadcrumb'] ? null : (string) $row['breadcrumb'], 'body_md' => (string) $row['body_md'], 'score' => (float) $row['score'], ], $rows); } /** * The `(type, uid)` of every fetched record whose text matches, for resolving free text * in the trip search. Unlike searchFulltext(), records without a URL count: a hotel with * no page of its own still leads to the products it belongs to. * * @param list $types * * @return list */ public function matchRecords(string $booleanQuery, array $types, string $language): array { $rows = $this->getEntityManager()->getConnection()->fetchAllAssociative( 'SELECT type, uid FROM content_document' .' WHERE fetch_state = :ok AND language = :language AND type IN (:types)' .' AND MATCH(title, breadcrumb, body_md) AGAINST(:query IN BOOLEAN MODE)', ['ok' => ContentFetchState::Ok->value, 'language' => $language, 'types' => $types, 'query' => $booleanQuery], ['types' => ArrayParameterType::STRING], ); return array_map(static fn (array $row): array => ['type' => (string) $row['type'], 'uid' => (int) $row['uid']], $rows); } /** * Fetched records of the given types in one language, keyed by `type:uid`. * * @param list $types * * @return array */ public function findRecords(array $types, string $language): array { /** @var list $documents */ $documents = $this->createQueryBuilder('d') ->where('d.type IN (:types)') ->andWhere('d.language = :language') ->andWhere('d.fetchState = :ok') ->setParameter('types', $types) ->setParameter('language', $language) ->setParameter('ok', ContentFetchState::Ok) ->getQuery() ->getResult(); $indexed = []; foreach ($documents as $document) { $indexed[$document->getRecordRef()] = $document; } return $indexed; } /** * The indexed document published at a URL. Trailing slashes are not significant. */ public function findOneIndexedByUrl(string $url): ?ContentDocument { $trimmed = rtrim($url, '/'); /** @var list $documents */ $documents = $this->createQueryBuilder('d') ->where('d.url IN (:urls)') ->andWhere('d.indexed = true') ->setParameter('urls', [$trimmed, $trimmed.'/']) ->orderBy('d.id', 'ASC') ->setMaxResults(1) ->getQuery() ->getResult(); return $documents[0] ?? null; } }