A única diferença, é que iremos renomear algumas coisas, passando os parâmetros.
Exemplo: ao invés de usar (1,3) para referenciar a ID das categorias, usaremos IN :categoryIds < parâmetro.
Outra coisa, como temos um pageable, precisamos usar o countQuery.
Como já temos o DISTINCT, no início faremos o SELECT COUNT(*) FROM (). E no final, como é uma subconsulta, usaremos o AS tb_result.
@Query(nativeQuery = true, value = """
SELECT DISTINCT tb_product.id, tb_product.name
FROM tb_product
INNER JOIN tb_product_category ON tb_product_category.product_id = tb_product.id
WHERE (:categoryIds IS NULL OR tb_product_category.category_id IN :categoryIds)
AND (LOWER(tb_product.name) LIKE LOWER(CONCAT('%',:name,'%')))
ORDER BY tb_product.name
""",
countQuery = """
SELECT COUNT(*) FROM (
SELECT DISTINCT tb_product.id, tb_product.name
FROM tb_product
INNER JOIN tb_product_category ON tb_product_category.product_id = tb_product.id
WHERE (:categoryIds IS NULL OR tb_product_category.category_id IN :categoryIds)
AND (LOWER(tb_product.name) LIKE LOWER(CONCAT('%',:name,'%')))
) AS tb_result
""")
Page<ProductProjection> searchProducts(List<Long> categoryIds, String name, Pageable pageable);
@Query("SELECT obj FROM Product obj JOIN FETCH obj.categories "
+ "WHERE obj.id IN :productIds ORDER BY obj.name")
List<Product> searchProductsWithCategories(List<Long> productIds);