sábado, 29 de mayo de 2010

SpellChecker en Ruby

Estoy utilizando Ferret para las búsquedas (Full text search) escrita en Ruby y C inspirada en Apache Lucene que está escrita en Java.
No encontré por ninguna parte la implementación de SpellChecker para Ferret(si realmente existe entonces me ayudó para entender mejor Lucene :-) ), así es que tomé el código de Lucene SpellChecker en java (versión 2.4.1) buscando también inspiración :-)

No está al 100%, ya que por ahora no considera la frecuencia, sólo considera la distancia de Levenshtein, pero para una primera versión del proyecto en el que la uso es suficiente (www.buskauto.com).

La implementación es la misma que en Lucene:
* se crean ngramas de las palabras para el diccionario.
* luego para sugerir una o más palabras se crean los ngramas de la palabra con error
* se buscan en el diccionario las palabras que coincidan con algún ngrama.
* y se prioriza por la distancia de Levenshtein con respecto a la palabra con error.



Se utiliza así:

dir = Store::RAMDirectory.new()
@spell = SpellChecker::SpellChecker.new(dir)
@spell.indexDictionary(reader, :texto)

@spell.suggestSimilar(word, 3, indexReader, field, false)

A continuación el código:

require 'ferret'
require 'text'

class String
  attr_accessor :___distance
  attr_accessor :___freq
end  

module SpellChecker
  include Ferret
  class SpellChecker
    
    def initialize(spellIndex)
      @spellIndex = spellIndex
      writer = Index::IndexWriter.new(:dir => @spellIndex)
      writer.close
      @searcher = Search::Searcher.new(@spellIndex)
    end
    
    def indexDictionary(reader, term)
      writer = Index::IndexWriter.new(:dir => @spellIndex, :analyzer =>  Analysis::WhiteSpaceAnalyzer.new)
      writer.field_infos.add_field(:word, :store => :yes)
      
      reader.terms(term).each{|word,b|
        if word.length >= 3 and !exist(word)
          doc = createDocument(word)
          writer << doc
        end
      }
      writer.optimize
      writer.close
      @searcher.close
      @searcher = Search::Searcher.new(@spellIndex)
    end
    
    def suggestSimilar(word, numSug, ir, field, more_popular)
      freq = 0
      if ir and field
        freq = field.respond_to?("collect") ? field.collect{|f| ir.doc_freq(f, word)}.max : ir.doc_freq(field, word)
      end
      #freq = (ir and field) ? ir.doc_freq(field, word) : 0
      goal_freq = (more_popular and ir and field) ? freq : 0 
      
      query = Search::BooleanQuery.new
      hash = createDocument(word)
      hash.each do |key, gram|
        add(query, key, gram)
      end
      
      min = 0.5
      pq = Utils::PriorityQueue.new(numSug) {|a, b| a.___distance > b.___distance}
      top_docs = @searcher.search(query)
      top_docs.hits.each do |hit|
        sug_word = @searcher[hit.doc][:word]
        next if sug_word.eql?(word)
        sug_word.___distance = 1.0 - (Text::Levenshtein.distance(sug_word, word) / [word.size, sug_word.size].max.to_f)
        next if sug_word.___distance < min
        
        if ir and field
          sug_word.___freq = field.respond_to?("collect") ? field.collect{|f| ir.doc_freq(f, sug_word)}.max : ir.doc_freq(field, sug_word)
          next if (more_popular and goal_freq > sug_word.___freq) or sug_word.___freq < 1
        end
        pq.insert(sug_word)
        min = pq.top.___distance if pq.size == numSug
      end
      
      a = Array.new(pq.size)
      a.collect{|x| pq.pop }
    end
    
    def add(bq, name, value)
      if value.respond_to?("each")
        value.each{|v|
          tq = Search::TermQuery.new(name, v)
          bq.add_query(tq, :should)
        }
      else
        tq = Search::TermQuery.new(name, value)
        bq.add_query(tq, :should)
      end
    end
    
    def createDocument(word)
      doc = {:word => word}
      doc = addGram(word, doc)
    end
    
    def addGram(word, doc)
      len = word.length
      ng1 = getMin(len)
      ng2 = getMax(len)
      
      ng1.upto(ng2) do |ng|
        key = "gram" + ng.to_s
        doc[key] = Array.new
        gend = nil
        0.upto(len - ng) do |i|
          gram = word[i..i+ng-1]
          doc[key] << gram
          doc["start" + ng.to_s] = gram if i == 0
          gend = gram;
        end
        doc["end" + ng.to_s] = gend if gend
      end
      doc
    end
    
    def getMin(l)
      if l > 5
        3
      elsif l == 5
        2
      else
        1
      end
    end
    def getMax(l)
      if l > 5
        4
      elsif l == 5
        3
      else
        2
      end
    end
    
    def exist(word)
      @searcher.doc_freq(:word, word) > 0
    end
end  


Leer más...

miércoles, 6 de mayo de 2009

Lucene crear índice y buscar

Ya que he estado trabajando con Lucene, lo mejor será anotar lo aprendido.
Este es un ejemplo básico de Lucene.

* Creación de un índice, en este caso se guarda en el índice la descripción del producto y su Id.


public void crearIndice(File indexDir, Analyzer analyzer, List productos) throws CorruptIndexException, LockObtainFailedException, IOException {
IndexWriter writer = new IndexWriter(indexDir, analyzer, true, IndexWriter.MaxFieldLength.UNLIMITED);
writer.setUseCompoundFile(false);

for (Producto producto : productos) {
Document doc = new Document();
doc.add(new Field("descripcion", producto.getDescripcion(), Field.Store.YES, Field.Index.ANALYZED));
doc.add(new Field("id", producto.getId() + "", Field.Store.YES, Field.Index.NO));
writer.addDocument(doc);
}
writer.optimize();
writer.commit();
writer.close();
}


indexDir: directorio donde se crearán los archivos de Lucene.
Document: el índice esta compuesto por documentos, en este ejemplo, cada documento representa un producto (id, descripción).
analyzer: analizador, en este ejemplo se usa StandarAnalyzer que transforma el texto a minúsculas, elimina los caracteres no alfanuméricos y separa las palabras, ver más abajo ejemplo de StandarAnalyzer.
Field.Store.YES: Se almacena el texto.
Field.Index.ANALYZED: Se analiza el texto, lo que permitirá buscar por una o más palabras del texto.

Ejemplo de StandarAnalyzer:

public class TestStandardAnalyzer {
public static void main(String[] args) throws IOException {
Analyzer analyzer = new StandardAnalyzer();
TokenStream resultado = analyzer.tokenStream("", new StringReader("Éste es un-texto + analizado // por StandarAnalyzer 123"));
Token token = new Token();
while ((token = resultado.next(token)) != null) {
System.out.println("analyzer: " + token.term());
}
}
}

El resultado es:

analyzer: éste
analyzer: es
analyzer: un
analyzer: texto
analyzer: analizado
analyzer: por
analyzer: standaranalyzer
analyzer: 123



* Búsqueda en el índice:


public List buscar(File indexDir, Analyzer analyzer, String texto) throws CorruptIndexException, IOException, ParseException {
List productos = new ArrayList();
IndexSearcher is = new IndexSearcher(FSDirectory.getDirectory(indexDir));
QueryParser qp = new QueryParser("descripcion", analyzer);
qp.setDefaultOperator(QueryParser.AND_OPERATOR);
Query query = qp.parse(texto);
TopDocCollector collector = new TopDocCollector(100);
is.search(query, collector);
ScoreDoc[] hits = collector.topDocs().scoreDocs;

for (int i = 0; i < hits.length; i++) {
int docId = hits[i].doc;
Document doc = is.doc(docId);
Producto p = new Producto();
p.setId(Integer.parseInt(doc.get("id")));
p.setDescripcion(doc.get("descripcion"));
productos.add(p);
}
is.close();
return productos;
}


QueryParser("descripcion", analyzer): indica por cual campo realizaremos la búsqueda, en este ejemplo sólo podemos buscar por "descripción".
qp.setDefaultOperator(QueryParser.AND_OPERATOR): se debe aplicar el operador lógico AND, es decir, si la búsqueda es por dos o más palabras, sólo debe entregar los documentos donde estén todas las palabras.
Query query = qp.parse(texto): Transforma el texto para su búsqueda. Ver más abajo ejemplo de QueryParser.
is.search(query, collector): ejecuta la búsqueda y el resultado queda en collector.

Ejemplo de QueryParser:

public class TestQueryParser {
public static void main(String[] args) throws ParseException {
QueryParser qp = new QueryParser("descripcion", new StandardAnalyzer());
qp.setDefaultOperator(QueryParser.AND_OPERATOR);
Query query = qp.parse("iPod nano");
System.out.println("Con AND_OPERATOR: " + query);
qp.setDefaultOperator(QueryParser.OR_OPERATOR);
query = qp.parse("iPod nano");
System.out.println("Con OR_OPERATOR: " + query);
}
}

El resultado es:

Con AND_OPERATOR: +descripcion:ipod +descripcion:nano
Con OR_OPERATOR: descripcion:ipod descripcion:nano



Ejemplo completo:

public class Indice {
public static void main(String[] args) throws CorruptIndexException, LockObtainFailedException, IOException, ParseException {
File indexDir = new File("/tmp/productos");
Analyzer analyzer = new StandardAnalyzer();
List productos = new ArrayList();
Producto p = new Producto(1, "iPod mini");
productos.add(p);
p = new Producto(2, "iPod shuffle");
productos.add(p);
p = new Producto(3, "iPod touch");
productos.add(p);

Indice indice = new Indice();
indice.crearIndice(indexDir, analyzer, productos);
System.out.println("Indice creado");

List resultado = indice.buscar(indexDir, analyzer, "ipod mini");
System.out.println("Resultado: " + resultado.size() + " producto(s)");
for (Producto producto : resultado) {
System.out.println("id: " + producto.getId() + " producto: " + producto.getDescripcion());
}
}

public void crearIndice(File indexDir, Analyzer analyzer, List productos) throws CorruptIndexException, LockObtainFailedException, IOException {
IndexWriter writer = new IndexWriter(indexDir, analyzer, true, IndexWriter.MaxFieldLength.UNLIMITED);
writer.setUseCompoundFile(false);

for (Producto producto : productos) {
Document doc = new Document();
doc.add(new Field("descripcion", producto.getDescripcion(), Field.Store.YES, Field.Index.ANALYZED));
doc.add(new Field("id", producto.getId() + "", Field.Store.YES, Field.Index.NO));
writer.addDocument(doc);
}
writer.optimize();
writer.commit();
writer.close();
}

public List buscar(File indexDir, Analyzer analyzer, String texto) throws CorruptIndexException, IOException, ParseException {
List productos = new ArrayList();
IndexSearcher is = new IndexSearcher(FSDirectory.getDirectory(indexDir));
QueryParser qp = new QueryParser("descripcion", analyzer);
qp.setDefaultOperator(QueryParser.AND_OPERATOR);
Query query = qp.parse(texto);
TopDocCollector collector = new TopDocCollector(100);
is.search(query, collector);
ScoreDoc[] hits = collector.topDocs().scoreDocs;

for (int i = 0; i < hits.length; i++) {
int docId = hits[i].doc;
Document doc = is.doc(docId);
Producto p = new Producto();
p.setId(Integer.parseInt(doc.get("id")));
p.setDescripcion(doc.get("descripcion"));
productos.add(p);
}
is.close();
return productos;
}
}




Leer más...

martes, 15 de abril de 2008

Mi AFP me dice que seré pobre.

Estoy exagerando con el título, cuando me jubile no estaré en la calle pero disminuirán considerablemente mis ingresos, veamos que me dice:

Mi AFP tiene en su página una sección llamada "proyección de pensión".
Esta proyección me dice que obtendré una pensión igual al 53% de mi sueldo bruto actual. El supuesto es una rentabilidad anual del 5% de mi fondo.

Estoy en el fondo A que, según la www.safp.cl, mi AFP logró una rentabilidad en Marzo de -4,03%, en Febrero de 4,63% y en Enero de -9,42%, por lo tanto el acumulado del 2008 es de -9,05%, espero que una rentabilidad anual del 5% lo consideren como el caso menos optimista :-) . Aunque en el 2007 la rentabilidad fue de 10%. ¿Quién me puede decir que ocurrirá en los próximos 30 o 40 años? :-)

El resultado de esta proyección de mi pensión está expresada en el valor actual de la moneda, por lo tanto, considerando la inflación, baja aún más mi pensión.
El IPC desde el 2000 al 2007 a aumentado un 22,67%, es decir, mis 100 pesos del año 2000 hoy valen menos de 78 pesos. (Valores del IPC en www.bcentral.cl).
Está bien, mi sueldo debería subir por lo menos un porcentaje similar al del IPC.

La jubilación se considera como una renta más, por lo tanto, está afecta a impuesto, es decir, baja aún más mi pensión.


Bien, esta es mi situación.
Puedo conformarme, "cuando me jubile mis gastos serán menores". Veamos:
* Contribuciones: Se me duplican, ya que habrá finalizado el plazo que indica el DFL2.
* Servicios básicos: siguen.
* Vacaciones: Ya que tendré tanto tiempo libre, mmm, me tendré que quedar en la casa.
* Salud: mmm, ¿debo seguir?. Acá me quedo con cero.


¿Quién tiene la culpa? ¿La AFP? ¿El gobierno? No, sólo yo :-)

Ahora sólo debo ver como cambiar esta situación, he escuchado alguna palabras que debo estudiar como:

* Cuenta de Ahorro (siempre que la tasa de interés sea mayor que la inflación ¿Existen? )
* APV
* Fondos Mutuos
* Depósitos a Plazo
* Acciones
* Inversiones
* Gastar menos :-), esto lo necesito para los puntos anteriores (ahorrar).
* Buscar otro trabajo
* Segundo trabajo



NOTA: Este porcentaje (pensión / sueldo) puede cambiar para otra persona que por ejemplo empezó a una edad más temprana a imponer.
También se debe considerar el tope imponible (60UF), ya que si tu sueldo es menor a este tope se impone por el 10% del sueldo, pero si es mayor al tope se impone por el 10% de 60UF. Visto de otra forma: una persona cuyo sueldo es mayor a 60UF va obtener una pensión (sin APV) igual a una persona cuyo sueldo es de 60UF.

Leer más...

lunes, 7 de abril de 2008

Java2Ruby

A veces descubro que aún sigo escribiendo en Java, así que espero que con estos apuntes pueda escribir en Ruby. :-)


Java:


for( int i = 0; i < 6; i++){
System.out.println(i);
}

Ruby:

3.times{|i| puts i}

Java:

String a = null;
String b = "hola";
if( a == null){
a = b;
}

Ruby:

b = "hola"
a = a || b


Java:

public static String join(Collection coleccion, String separador) {
StringBuffer buffer = new StringBuffer();
Iterator iterador = coleccion.iterator();
while (iterador.hasNext()) {
buffer.append(iterador.next());
if (iterador.hasNext()) {
buffer.append(separador);
}
}
return buffer.toString();
}
String b = join(a, ",");

Ruby:

a = [1,2,3,4,5]
b = a * ","


¿Superaré las dos líneas en Ruby? :-)

Leer más...

jueves, 20 de marzo de 2008

¿ Dónde está mi timbre y mi estampilla ?

¿ Timbre y estampilla ?

No, es otro IVA :-)

Donde no está el IVA está este impuesto y a veces se ponen de acuerdo para estar los dos.

No pagas IVA, por ejemplo, al obtener un crédito en un banco, entonces pagas este impuesto. Pero cuando gastas este crédito pagas el IVA.

Buscando cómo se cálcula este impuesto, en particular para los créditos de consumo encontré esto en www.sii.cl:



"El Impuesto de Timbres y Estampillas se aplica a los documentos o actos que involucran una operación de crédito de dinero, por ejemplo letras de cambio o pagarés. La base imponible es el monto del capital especificado en cada documento. La tasa del impuesto es variable dependiendo del período que medie entre la emisión del documento y el plazo de vencimiento de éste, la cual es de 0,134% por el valor del documento por cada mes o fracción del plazo, con un tope máximo de 1,608%. Los cheques y los protestos de pagarés están gravados con una cantidad fija por concepto de Impuesto de Timbres y Estampillas."

Las tasas cambiaron para los años 2007, 2008 y 2009 en adelante.
2007 mínimo 0,125% y máximo 1,5%
2008 mínimo 0,1125% y máximo 1,35%
2009 en adelante mínimo 0,1% y máximo 0,2%

Si entiendo bien, para el caso de un crédito de consumo de 6 meses, para este año, la tasa será:

6 * 0,1125% = 0,675% sobre el total del crédito

y para uno de 12 meses: 12 * 0.1125% = 1,35% sobre el total del crédito.

En 12 cuotas llegamos al máximo, entonces de 12 o más cuotas la tasa será la misma (1,35%).

Por ejemplo para un crédito de 7.000.000 tendremos que gravar un impuesto de 94.500.

Pero no es así.

Revisando los simuladores de algunos bancos veo que antes de calcular este impuesto se agregan otros cargos, por ejemplo:

(C) 7.000.000 = crédito
(N) 2.000 = notario
(S) 120.000 = seguro de desgravamen, depende del banco, es alredor de 2.500 por mes, en este caso la simulación fue en 48 cuotas.

Bien, cómo el trato con el banco es un crédito con cuotas iguales, no vamos a pagar el impuesto ni el notario al tomar el crédito, si no que también estos montos se van al total del crédito. Uff :-)
Ah, pero agreguemos también el seguro de desgravamen. :-)

Llamemos (I) al impuesto.
Tenemos entonces que el total del crédito es: (1) TC = C + N + S + I
Por lo tanto aplicamos la tasa a TC: (2) I = TC * p,
con p = 1,35/100 = 0,0135

Si unimos (1) y (2) obtenemos: (3) TC = C + N + S + (TC * p)

Moviendo para un lado:
(4) TC - TC*p = C + N + S
(5) TC (1 - p) = C + N + S
(6) TC = (C + N + S) / (1-p)

Con (6) obtenemos el total del crédito TC = 7.219.463

y con (2) obtienes tu timbre y estampilla I = 97.463 :-)


Uff, cuando ha aumentado nuestro crédito y aún nos faltan los intereses. :-(


Nota: hay simuladores que aún están usando la tasa del 2007.






Leer más...