Skip to content
This repository
Browse code

first commit

  • Loading branch information...
commit 6014a1ccc303893a0ed19c6409844ecbf14d7f47 0 parents
Pablo Quiros Solis authored
200 REAME
... ... @@ -0,0 +1,200 @@
  1 +============================================
  2 +Función de busqueda: usando hash o sqlite3?
  3 +============================================
  4 +
  5 +Muchas veces necesitamos tener una función que devuelva varios valores usando como parámetro uno solo, la mayoría de las veces estás son pequeñas y se almacena usando hash, y si es más grande usualmente se utiliza una base de datos donde se monta una tabla con llave primaria el valor en el búsqueda (por lo tanto única).
  6 +Este pequeño articulo no pretende predicar uno o el otro, sino mostrar su beneficios tanto en flexibilidad como en rendimiento.
  7 +Cuando estamos hablando de grande cantidades de datos usualmente optamos por el uso de bases de datos, pero a veces estos motores salen sobrando para lo que realmente necesitamos (más ahora que la memoria RAM viene en gigas). Por esta razón se me vino a la mente sqlite que es una micro-base de datos (en su versión 3-3-6-10 pesa tan solo 206.85KiB) gratuita disponible en todos los sistemas operativos de mayor uso (linux, mac, windows).
  8 +En este caso solo necesitamos tener los binarios accesibles y poder construir librería que manejen considerable cantidad de datos disponibles sin instalar grandes motores de base de datos para tareas simples, como por ejemplo funciones de conversiones de datos de alto costo computacional pre-computadas.
  9 +El problema especifico se basa es la necesidad de una función que reciba un nombre de una empresa y devuelva este nombre en su versión "estandar".
  10 +
  11 +veamos el siguiente ejemplo:
  12 +
  13 +Mac Donalds
  14 +Restaurantes Mac Donalds
  15 +Rest Mac Donalds
  16 +Mac Donalds S.A.
  17 +
  18 +La función que necesitamos recibiría estos nombres y devolvería su nombre "estandar" que sería "Mac Donalds" para todos los casos anteriores.
  19 +Esto es muy común en limpieza de datos. En mi caso estas relaciones de estandarización fueron hechas por personas "a pata" ayudados por software.
  20 +La herramienta utiliza estos metadatos como fuente, y se vería maso menos así:
  21 +
  22 +std_name('Mac Donalds') => 'Mac Donalds'
  23 +std_name('Restaurantes Mac Donalds') => 'Mac Donalds'
  24 +std_name('Rest Mac Donalds') => 'Mac Donalds'
  25 +std_name('Mac Donalds S.A.') => 'Mac Donalds'
  26 +
  27 +Los metadatos están en un archivo CSV. Se realizaron dos versiones una usando hash's y array's en memoria y otra usando sqlite3.
  28 +
  29 +Estas realizan los siguientes tipos de búsqueda:
  30 +
  31 + * búsqueda exacta: la versión por match exacto devuelve el standard name en donde el nombre es exactamente igual al suministrado.
  32 +
  33 + * búsqueda aproximada al estilo 'like': en este caso devolverá el primer match en donde la palabra suministrada este contenida dentro del campo de búsqueda, al estilo de name "like '%campo_parametro%'”
  34 +
  35 +
  36 +=================
  37 +Versión Hash
  38 +=================
  39 +
  40 +archivo: std_names_hash.rb
  41 +STD_NAME = {}
  42 +File.new("#{File.dirname(__FILE__)}/STD_NAMES_MASTER.csv",'r').each{|line|
  43 +record = line.gsub(/[\n\r]/,'').split(",",-1)
  44 +STD_NAME[record[0]]=[record[1],record[2]]
  45 +}
  46 +STD_NAME_FLAT = STD_NAME.to_a
  47 +def std_names_fake_like name
  48 +res = STD_NAME_FLAT.detect{|v| v[0] =~ Regexp.new("^.*(#{name}).*$")}
  49 +res||=[nil,[nil,nil]]
  50 +res[1]
  51 +end
  52 +
  53 +
  54 +La preparación del Hash es bastante simple, un simple barrido secuencial archivo e su inserción. Para simular búsquedas estilo like convertí el hash en array para poder utilizar el método detect en conjunto con expresiones regulares. El método detect devuelve el primer valor que haga match en este caso con la expresión regular "^.*(valor).*$" usando el objeto Regexp, esta expresión regular equivale al "like '%valor%'" de sql.
  55 +
  56 +=================
  57 +Versión Sqlite3
  58 +=================
  59 +_______
  60 +Nota: se necesita tener creada la base de datos corriendo el siente comando:
  61 +
  62 +~#sqlite3 test.db
  63 +SQLite version 3.4.2
  64 +Enter ".help" for instructions
  65 +sqlite> .quit
  66 +
  67 +Le damos '.quit' para salir.
  68 +_______
  69 +
  70 +archivo: std_names_sqlite3.rb
  71 +`sqlite3 test.db "drop table std_names"`
  72 +`sqlite3 test.db "create table std_names(std_name TEXT,bus_name TEXT,sic TEXT)"`
  73 +`sqlite3 -separator , test.db ".import STD_NAMES_MASTER.csv std_names"`
  74 +`sqlite3 test.db "create index au_name_idx on std_names (std_name)"`
  75 +require 'dbi'
  76 +@dbh = DBI.connect('DBI:SQLite3:test.db', '', '')
  77 +def std_names name
  78 +@dbh.select_one("select bus_name,sic from std_names where std_name = '#{name}';") rescue [nil,nil,nil]
  79 +end
  80 +def std_names_with_like name
  81 +@dbh.select_one("select bus_name,sic from std_names where std_name like '%#{name}%';") rescue [nil,nil,nil]
  82 +end
  83 +
  84 +
  85 +La primera linea borra la tabla en caso de que exista, la segunda crea la tabla, la tercera inserta el archivo CSV y la cuarta crea un índice sobre la columna de búsqueda. Están definidos dos métodos, uno para búsquedas exactas y otro para busquedas utilizando like, estas dos devolviendo la primera ocurrencia que haga match.
  86 +
  87 +Para verificar que esta corriendo correctamente corrí cuatro variaciones de nombres de empresas que son la misma (todas deberian de tener el mismo nombre estándar de empresa) para match, y un par de ejemplo match por aproximación con un segmento del nombre de la empresa.
  88 +
  89 +archivo: test_run_std_names.rb
  90 +require 'std_names_hash'
  91 +require 'std_names_sqlite3'
  92 +puts "exact match:"
  93 +['SUPERCUTS 90198','SUPERCUTS 90250','SUPERCUTS 90471','SUPERCUTS 9765'].each{|business_name|
  94 +puts "hash: #{STD_NAME[business_name][0]}"
  95 +puts "sqlite3: #{std_names(business_name)[0]}"
  96 +}
  97 +puts "using like (or fake like):"
  98 +puts "Using sqlite3:"
  99 +puts std_names_with_like('PERCU')[0]
  100 +puts std_names_with_like('PERCUTS 9')[0]
  101 +puts "Using array, detect and regular expresion:"
  102 +puts std_names_fake_like('PERCU')[0]
  103 +puts std_names_fake_like('PERCUTS 9')[0]
  104 +
  105 +Después de correrlo dio los siguientes resultados:
  106 +
  107 +exact match:
  108 +hash: SUPERCUTS
  109 +sqlite3: SUPERCUTS
  110 +hash: SUPERCUTS
  111 +sqlite3: SUPERCUTS
  112 +hash: SUPERCUTS
  113 +sqlite3: SUPERCUTS
  114 +hash: SUPERCUTS
  115 +sqlite3: SUPERCUTS
  116 +using like (or fake like):
  117 +Using sqlite3:
  118 +SUPERCUTS
  119 +SUPERCUTS
  120 +Using array, detect method and regular expresion:
  121 +SUPERCUTS
  122 +SUPERCUTS
  123 +
  124 +
  125 +Hasta el momento las dos implementaciones soluciona el problema. Para medir cual de las dos solucionan mejor problema realicé pruebas de rendimiento, a tres niveles, carga de datos, match exacto y match aproximado. Esto se resume en el siguiente script.
  126 +
  127 +archivo: test_hash_vs_sqlite3.rb
  128 +require 'benchmark'
  129 +puts 'load time:'
  130 +Benchmark.bm do |x|
  131 +x.report("hash: ") { require 'std_names_hash'}
  132 +x.report("sqlite3:") {require 'std_names_sqlite3'}
  133 +end
  134 +data = []
  135 +File.new('test_data.csv','r').each{|line| data.push line.split('","',-1)[9] if line.split('","',-1)[9]}
  136 +puts 'exact match:'
  137 +Benchmark.bm do |x|
  138 +x.report("hash: "){
  139 +data.each{|bus_name| STD_NAME[bus_name]}
  140 +}
  141 +x.report("sqlite3:"){
  142 +data.each{|bus_name| std_names bus_name}
  143 +}
  144 +end
  145 +puts 'match using like:'
  146 +Benchmark.bm do |x|
  147 +x.report("hash: "){
  148 +data.each{|bus_name| std_names_fake_like(bus_name)}
  149 +}
  150 +x.report("sqlite3:"){
  151 +data.each{|bus_name| std_names_with_like(bus_name)}
  152 +}
  153 +end
  154 +
  155 +El primer benchmark mide el tiempo de carga en los dos casos, el recorrido secuencial para la implementación mediante hash y array, y la carga de datos a sqlite y la creación del indice sobre la tabla (con el comando: sqlite3 -separator , test.db ".import STD_NAMES_MASTER.csv std_names").
  156 +El segundo mide el tiempo de las funciones de búsqueda "exacta" en hash y con sqlite. Para esta se utiliza datos de prueba en el archivo test_data.csv que contiene 10000 registros.
  157 +El tercer benchmark mide el tiempo de ejecución de las funciones de busqueda con expresiones regulares para el caso del archivo y array, y el uso de la sentencia "like" en sql, para una búsqueda aproximada con un segmento del la llave. En este ulitmo caso se utilizaron 100 datos de prueba.
  158 +Esto corrió en una Intel(R) Core(TM)2 Duo CPU T7100 @ 1.80GHz, con 2066088 kB de memoria.
  159 +
  160 +Estos fueron los resultados de correr el script test_hash_vs_sqlite3.rb:
  161 +
  162 +load time:
  163 +user system total real
  164 +hash: 3.500000 0.130000 3.630000 ( 3.677747)
  165 +sqlite3: 0.360000 0.060000 5.340000 ( 21.539347)
  166 +
  167 +exact match (test data 10000 rows):
  168 +user system total real
  169 +hash: 0.020000 0.000000 0.020000 ( 0.022092)
  170 +sqlite3: 7.260000 0.430000 7.690000 ( 7.856472)
  171 +
  172 +match using like (test data 100 rows):
  173 +user system total real
  174 +hash: 599.450000 10.640000 610.090000 (627.246236)
  175 +sqlite3: 24.260000 2.530000 26.790000 ( 27.629173)
  176 +
  177 +
  178 +======================================
  179 +Con los resultados podemos concluir:
  180 +======================================
  181 +
  182 +Funciones de búsqueda exacta:
  183 +
  184 + * utilice hash definitivamente
  185 +
  186 +
  187 +Función de búsqueda aproximada:
  188 +
  189 + * Utilice la solución con motor de base de datos
  190 + * Si tiene un motor de bases de datos instalado utilicelo, sino utilice algún motor portarle, gratuito y funcionará con tiempos respuesta bastante aceptables.
  191 +
  192 +
  193 +Si necesita las dos:
  194 +
  195 + * combine las dos opciones para búsqueda exacta utilice hash, y para aproximadas la versión con base de datos, en serio vale la pena combinar la solución ya que los hash son invencibles en match exactos.
  196 +
  197 + Otras:
  198 +
  199 + * Las funciones de match exacto son realmente eficientes, los datos de prueba fueron 10000 y corrieron en 3.7 segundos es decir 2702 matcheos por segundo, para la versión de sqlite3 465.1 registros por segundo. Las funciones de match aproximado son mucho más lentas de los de prueba fueron mucho menos y duró considerablemente más que los otros tipos de match.
  200 + * Para el caso de la versión array-expresion regular tomó 627.2 segundos para computar 100 entradas, lo que corresponde a 0.159 por segundo, mientras que la versión sqlite con like 3.6 registros por segundo.
13 std_names_hash.rb
... ... @@ -0,0 +1,13 @@
  1 +STD_NAME = {}
  2 +File.new("#{File.dirname(__FILE__)}/STD_NAMES_MASTER.csv",'r').each{|line|
  3 + record = line.gsub(/[\n\r]/,'').split(",",-1)
  4 + STD_NAME[record[0]]=[record[1],record[2]]
  5 +}
  6 +STD_NAME_FLAT = STD_NAME.to_a
  7 +def std_names_fake_like name
  8 + res = STD_NAME_FLAT.detect{|v| v[0] =~ Regexp.new("^.*(#{name}).*$")}
  9 + res||=[nil,[nil,nil]]
  10 + res[1]
  11 +end
  12 +
  13 +
12 std_names_sqlite3.rb
... ... @@ -0,0 +1,12 @@
  1 +`sqlite3 test.db "drop table std_names"`
  2 +`sqlite3 test.db "create table std_names(std_name TEXT,bus_name TEXT,sic TEXT)"`
  3 +`sqlite3 -separator , test.db ".import STD_NAMES_MASTER.csv std_names"`
  4 +`sqlite3 test.db "create index au_name_idx on std_names (std_name)"`
  5 +require 'dbi'
  6 +@dbh = DBI.connect('DBI:SQLite3:test.db', '', '')
  7 +def std_names name
  8 + @dbh.select_one("select bus_name,sic from std_names where std_name = '#{name}';") rescue [nil,nil,nil]
  9 +end
  10 +def std_names_with_like name
  11 + @dbh.select_one("select bus_name,sic from std_names where std_name like '%#{name}%';") rescue [nil,nil,nil]
  12 +end
29 test_hash_vs_sqlite3.rb
... ... @@ -0,0 +1,29 @@
  1 +require 'benchmark'
  2 +puts 'load time:'
  3 +Benchmark.bm do |x|
  4 + x.report("hash: ") { require 'std_names_hash'}
  5 + x.report("sqlite3:") {require 'std_names_sqlite3'}
  6 +end
  7 +data = []
  8 +File.new('test_data.csv','r').each{|line| data.push line.split('","',-1)[9] if line.split('","',-1)[9]}
  9 +puts "exact match (test data #{data.length} rows):"
  10 +Benchmark.bm do |x|
  11 + x.report("hash: "){
  12 + data.each{|bus_name| STD_NAME[bus_name]}
  13 + }
  14 + x.report("sqlite3:"){
  15 + data.each{|bus_name| std_names bus_name}
  16 + }
  17 +end
  18 +puts "match using like (test data #{data[0..99].length} rows):"
  19 +Benchmark.bm do |x|
  20 + x.report("hash: "){
  21 + data[0..99].each{|bus_name| std_names_fake_like(bus_name)}
  22 + }
  23 + x.report("sqlite3:"){
  24 + data[0..99].each{|bus_name| std_names_with_like(bus_name)}
  25 + }
  26 +end
  27 +
  28 +@dbh.disconnect
  29 +
14 test_run_std_names.rb
... ... @@ -0,0 +1,14 @@
  1 +require 'std_names_hash'
  2 +require 'std_names_sqlite3'
  3 +puts "exact match:"
  4 +['SUPERCUTS 90198','SUPERCUTS 90250','SUPERCUTS 90471','SUPERCUTS 9765','QUIZNOS CLASSIC SUBS'].each{|business_name|
  5 + puts "hash: #{STD_NAME[business_name][0]}" ; puts "sqlite3: #{std_names(business_name)[0]}"
  6 +}
  7 +puts "using like (or fake like):"
  8 +puts "Using sqlite3:"
  9 +puts std_names_with_like('PERC')[0]
  10 +puts std_names_with_like('PERCUTS 9')[0]
  11 +puts "Using array, detect method and regular expresion:"
  12 +puts std_names_fake_like('PERC')[0]
  13 +puts std_names_fake_like('PERCUTS 9')[0]
  14 +

0 comments on commit 6014a1c

Please sign in to comment.
Something went wrong with that request. Please try again.