How to reset usual $query on search page to push custom $wpdb query there?

This is not how pre_get_posts works.

The pre_get_posts action gives developers access to the $query object
by reference (any changes you make to $query are made directly to the
original object – no return value is necessary
).

https://codex.wordpress.org/Plugin_API/Action_Reference/pre_get_posts

What you are doing is simply wrong. You don’t “return” another query via pre_get_posts, you alter the existing query. I don’t know what is so complex you can’t do it via an appropriate set of filters but if you must have some custom SQL then:

function project_modify_default_search( $query ) {
  if( $query->is_search && $query->is_main_query() && ! isset( $_REQUEST['search'] ) ) {

    //I'm in my search.php - confirmed

    //Reset default $query - not to execute at all

    //Do my custom complex query and pass the query as a $wp_query object
    //so that I can use the default search.php template
    global $wpdb;
    $searchQ = sanitize_text_field( get_query_var( 's' ) );

    // Sample "complex" query
    $complex_query = "SELECT SQL_CALC_FOUND_ROWS {$wpdb->posts}.ID FROM {$wpdb->posts} WHERE {$wpdb->posts}.post_title LIKE '$searchQ%' LIMIT 10";

    // Use `get_col` to return a simple array of IDs
    $custom_search_query = $wpdb->get_col( $complex_query );

    // Pass those IDs to the main query
    $query->set('post__in',$custom_search_query);

    // can't kill the default search here or it can break some functions.
    // $query->set('s',NULL);

    // So we do this:
    add_action( 'posts_search', '__return_empty_string' );

  } //endif
}
add_action( 'pre_get_posts', 'project_modify_default_search' );

Again, pretty sure this is the wrong way to generate a complicated query, especially given that you have at least one extra query relative to modifying the main query via a set of filters– posts_where, posts_join, etc.