Dividir una columna de cadena en varias variables ficticias

Como usuario relativamente inexperto del paquete data.table en R, he estado tratando de procesar una columna de texto en un gran número de columnas de indicadores (variables ficticias), con un 1 en cada columna que indica que una subcadena en particular era encontrado dentro de la columna de cadena. Por ejemplo, quiero procesar esto:

ID     String  
1       a$b  
2       b$c  
3       c  

dentro de esto:

ID     String     a     b     c  
1       a$b       1     1     0  
2       b$c       0     1     1  
3        c        0     0     1  

He descubierto cómo hacer el procesamiento, pero lleva más tiempo de lo que quisiera y sospecho que mi código es ineficiente. A continuación se muestra una versión reproducible de mi código con datos ficticios. Tenga en cuenta que en los datos reales, hay más de 2000 subcadenas para buscar, cada subcadena tiene aproximadamente 30 caracteres y puede haber hasta unos pocos millones de filas. Si es necesario, puedo poner en paralelo y lanzar muchos recursos al problema, pero quiero optimizar el código tanto como sea posible. He intentado ejecutar Rprof, lo que no sugiere mejoras obvias (para mí).

set.seed(10)  
elements_list <- c(outer(letters, letters, FUN = paste, sep = ""))  
random_string <- function(min_length, max_length, separator) {  
    selection <- paste(sample(elements_list, ceiling(runif(1, min_length, max_length))), collapse = separator)  
    return(selection)  
}  
dt <- data.table(id = c(1:1000), messy_string = "")  
dt[ , messy_string := random_string(2, 5, "$"), by = id]  
create_indicators <- function(search_list, searched_string) {  
    y <- rep(0, length(search_list))  
    for(j in 1:length(search_list)) {  
        x <- regexpr(search_list[j], searched_string)  
        x <- x[1]  
        y[j] <- ifelse(x > 0, 1, 0)  
    }  
    return(y)  
}  
timer <- proc.time()  
indicators <- matrix(0, nrow = nrow(dt), ncol = length(elements_list))  
for(n in 1:nrow(dt)) {  
    indicators[n, ] <- dt[n, create_indicators(elements_list, messy_string)]  
}  
indicators <- data.table(indicators)  
setnames(indicators, elements_list)  
dt <- cbind(dt, indicators)  
proc.time() - timer  

user  system elapsed 
13.17    0.08   13.29 

EDITAR

Gracias por las excelentes respuestas, todas muy superiores a mi método. Los resultados de algunas pruebas de velocidad a continuación, con ligeras modificaciones en cada función para usar 0L y 1L en mi propio código, para almacenar los resultados en tablas separadas por método y para estandarizar el pedido. Estos son tiempos transcurridos desde las pruebas de una sola velocidad (en lugar de medianas desde muchas pruebas), pero las carreras más grandes llevan mucho tiempo.

Number of rows in dt     2K      10K      50K     250K      1M   
OP                       28.6    149.2    717.0   
eddi                     5.1     24.6     144.8   1950.3  
RS                       1.8     6.7      29.7    171.9     702.5  
Original GT              1.4     7.4      57.5    809.4   
Modified GT              0.7     3.9      18.1    115.2     473.9  
GT4                      0.1     0.4      2.26    16.9      86.9

Claramente, la versión modificada del enfoque de GeekTrader es la mejor. Todavía soy un poco vago sobre lo que está haciendo cada paso, pero puedo repasarlo a mi antojo. Aunque algo fuera de los límites de la pregunta original, si alguien quiere explicar qué están haciendo de manera más eficiente los métodos de GeekTrader y Ricardo Saporta, sería apreciado por mí y probablemente por cualquiera que visite esta página en el futuro. Estoy particularmente interesado en entender por qué algunos métodos se escalan mejor que otros.

***** EDITAR # 2 *****

Intenté editar la respuesta de GeekTrader con este comentario, pero parece que no funciona. Hice dos modificaciones menores a la función GT3, a) ordenar las columnas, lo que agrega una pequeña cantidad de tiempo, yb) reemplazar 0 y 1 con 0L y 1L, lo que acelera un poco las cosas. Llame a la función resultante GT4. Tabla anterior editada para agregar tiempos para GT4 en diferentes tamaños de tabla. Claramente el ganador por una milla, y tiene la ventaja adicional de ser intuitivo.

Respuestas a la pregunta(6)

Su respuesta a la pregunta