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
sábado 29 de mayo de 2010
SpellChecker en Ruby
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, Listproductos) 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 Listbuscar(File indexDir, Analyzer analyzer, String texto) throws CorruptIndexException, IOException, ParseException {
Listproductos = 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();
Listproductos = 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");
Listresultado = 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, Listproductos) 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 Listbuscar(File indexDir, Analyzer analyzer, String texto) throws CorruptIndexException, IOException, ParseException {
Listproductos = 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.
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? :-)
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.