El proceso de clasificación se encarga de recorrer las notas extraídas que aún no fueron clasificadas, y de acuerdo a las pautas vigentes, clasificarlas como noticias relevantes.
87
Para realizar esta tarea es necesario contar con un motor de full text search para que las búsquedas en la base de datos sean óptimas y no tengan problemas de performance. Por lo tanto, es necesario identificar los diferentes motores de full text search que ofrecen los proveedores de cloud, y de qué forma se pueden utilizar para resolver los mecanismos de búsqueda.
APP ENGINE
El servicio Cloud SQL de App Engine al estar basado en MySQL, permite utilizar las capacidades de full text search de este motor.
Tanto las notas como las noticias, no están relacionadas con ningún tenant en particular, por lo que las tablas no necesitan el campo IdTenant para identificar el tenant. Las noticias se relacionan con un tenant al momento de informar una alerta.
Abajo se muestra un ejemplo de creación de la tabla Nota, con FullTextSearch en MySQL: CREATETABLE Nota ( IdNota integer, Titulo text, TextoCompleto text, IdExtraccion integer, IdTema integer,
PRIMARYKEY RESULTS_PK (IdNota), FULLTEXT (Titulo,TextoCompleto)
) ENGINE=MyISAM DEFAUTextoCompletoLT CHARSET=utf8; Código Fuente 7-30
En la figura de abajo se muestra el código Java para obtener los Id de las notas que coinciden con el patrón de búsqueda:
public Iterable<int> searchIdsNotas(String query) { List <int> results = new ArrayList<int>(); Connection conn = null;
try {
DriverManager.registerDriver(new AppEngineDriver());
conn = DriverManager.getConnection("jdbc:google:rdbms://[your db instance]/search_values");
String statement = "SELECT IdNota FROM Nota WHERE MATCH (Titulo,TextoCompleto) AGAINST (? WITH QUERY EXPANSION);";
PreparedStatement stmt = conn.prepareStatement(statement); stmt.setString(1, query); ResultSet rs = stmt.executeQuery(); while (rs.next()) { results.add(rs.getInt(1)); }
88 } catch (SQLException e) { e.printStackTrace(); } catch (EntityNotFoundException e) { e.printStackTrace(); } finally { if (conn != null) try { conn.close();
} catch (SQLException ignore) {} }
return results; }
Código Fuente 7-31
El proceso de clasificación se ejecutará desde un Servlet, y este será lanzado cuando se inserte un elemento en la cola ClasificacionQueue definida en el proceso de extracción.
Abajo se muestra la definición de la cola ClasificacionQueue en el archivo queues.xml <queue-entries>
<queue>
<name>ClasificacionQueue</name>
<max-concurrent-requests>2</max-concurrent-requests> </queue>
</queue-entries>
. Código Fuente 7-32
Definición del ServletClasificacionServlet en el archivo web.xml <servlet>
<servlet-name>ClasificacionServlet</servlet-name>
<servlet-class>com.example.alertasonline.cron.ClasificacionServlet</servlet-class> </servlet>
<servlet-mapping>
<servlet-name>ClasificacionServlet</servlet-name> <url-pattern>/clasificacion</url-pattern>
</servlet-mapping> </servlet>
Código Fuente 7-33
El proceso de clasificación se debe ejecutar después de que haya ocurrido el proceso de extracción. Para ello, en App Engine, podemos utilizar una cola de trabajo, para disparar la ejecución del proceso de clasificación.
89 package com.example.alertasonline; import java.io.IOException; import java.util.logging.Logger; import javax.servlet.ServletException; import javax.servlet.http.*; @SuppressWarnings("serial")
publicclassClasificacionServlet extends HttpServlet {
privatestaticfinal Logger _logger = Logger.getLogger(ClasificacionServlet.class.getName());
publicvoid doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
try {
clasificacionService.clasificarNotas();
}
catch (Exception ex) {
//Log any exceptions in your Cron Job }
}
@Override
publicvoiddoPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp); }
}
Código Fuente 7-34
Dado que el proceso de clasificación puede demorar más tiempo que el límite permitido de un request para una instancia FrontEnd, se utilizará el mismo backend definido para el módulo de facturación.
<backends>
<backend name="alertas_backend"> <class>B1</class>
<instances>1</instances> <options>
<dynamic>true</dynamic> </options>
</backend> </backends>
90
El enlace entre el módulo de extracción y el módulo de clasificación, queda en evidencia en el código que se ejecutará luego de hacer la extracción y que se transcribe abajo:
publicvoid dispararClasificacion(){
Queue queue = QueueFactory.getQueue("ClasificacionQueue");
TaskOptions taskOptions = TaskOptions.Builder.withUrl("/clasificacion") .header("Host", BackendServiceFactory.getBackendService().getBackendAddress("alertas_backend")) .method(Method.POST); queue.add(taskOptions); } Código Fuente 7-36 WINDOWS AZURE
El servicio SQL Azure Database si bien comparte muchas de las características de la versión SQL Server, la capacidad de Full Text Search es una de las funcionalidades que no es compatible, aunque existen alternativas open source para poder implementar esta funcionalidad. Uno de los productos open source que más apoyo tienen de la comunidad es Lucene.NET.
Lucene.NET es la implementación en C# para .net del producto Lucene escrito en java. Lucene.NET no está integrado directamente con el motor de SQL Azure Database, sino que tiene una arquitectura propia para manejar Full Text Search, por lo que es necesario realizar un proceso de extracción de los datos de la tabla de la base de datos para insertarlos e indexarlos en los archivos que utiliza Lucene.NET.
Abajo se muestra el código que accede a la base de datos SQL Azure Database e indexa los datos en la base de datos de Lucene.NET
publicvoid IndexarNotas() {
// Create the AzureDirectory against blob storage and create a catalog named 'Catalog' AzureDirectory azureDirectory= new
AzureDirectory(CloudStorageAccount.FromConfigurationSetting("Microsoft.WindowsAzure.Plugins. Diagnostics.ConnectionString"), "Catalog");
IndexWriter indexWriter = new IndexWriter(azureDirectory, new StandardAnalyzer(), true); indexWriter.SetRAMBufferSizeMB(10.0);
indexWriter.SetUseCompoundFile(false); indexWriter.SetMaxMergeDocs(10000); indexWriter.SetMergeFactor(100);
91 // Create a DataSet and fill it from SQL Database
DataSet ds = new DataSet();
using(SqlConnection sqlCon = new SqlConnection(sqlConnString)) {
sqlCon.Open();
SqlCommand sqlCmd = new SqlCommand(); sqlCmd.Connection = sqlCon;
sqlCmd.CommandType = CommandType.Text;
sqlCmd.CommandText = "select IdNota, TextoCompleto from Nota where indexada = false";
SqlDataAdapter sqlAdap = new SqlDataAdapter(sqlCmd); sqlAdap.Fill(ds); } if (ds.Tables[0] != null) { DataTable dt = ds.Tables[0]; if (dt.Rows.Count > 0) {
foreach (DataRow dr in dt.Rows) {
// Create the Document object
Document doc = new Document();
foreach (DataColumn dc in dt.Columns) {
// Populate the document with the column name and value from our query doc.Add(new Field(
dc.ColumnName,
dr[dc.ColumnName].ToString(), Field.Store.YES,
Field.Index.TOKENIZED)); }
// Write the Document to the catalog
indexWriter.AddDocument(doc); }
} }
// Close the writer indexWriter.Close();
this.MarcarNotasIndexadas(); }
Código Fuente 7-37
Abajo se muestra el proceso de búsqueda de FullTextSearch de Lucene
public List<int> searchIdsNotas() {
92 List<int> result = new List<int>();
// Create the AzureDirectory for blob storage AzureDirectory azureDirectory = new
AzureDirectory(CloudStorageAccount.FromConfigurationSetting("Microsoft.WindowsAzure.Plugins. Diagnostics.ConnectionString"), "Catalog");
// Create the IndexSearcher
IndexSearcher indexSearcher = new IndexSearcher(azureDirectory); // Create the QueryParser, setting the default search field to 'Bio'
QueryParser parser = new QueryParser("TextoCompleto", new StandardAnalyzer()); // Create a query from the Parser
Query query = parser.Parse(searchString); // Retrieve matching hits
Hits hits = indexSearcher.Search(query);
// Loop through the matching hits, retrieving the document
for (int i = 0; i < hits.Length(); i++) {
//Retrieve the string value of the 'Id' field from the
result.add(hits.Doc(i).GetField("IdNota").IntValue()); }
}
Código Fuente 7-38
Tanto App Engine como Windows Azure ofrecen alternativas para implementar Full Text Search. App Engine tiene una ventaja que al usar las características de Full Text Search de MySQL sin la necesidad de utilizar un producto aparte, como Windows Azure con Lucene, lo que puede facilitar el desarrollo de esta funcionalidad y el mantenimiento a futuro.
En la siguiente subsección se analizarán los mecanismos para enviar alertas a través de emails.