Desde la introducción de Embedded Python siempre ha estado la duda sobre su rendimiento respecto a ObjectScript y en más de una ocasión lo he comentado con @Guillaume Rongier , pues bien, aprovechando que estaba haciendo una pequeña aplicación para capturar los datos de los concursos públicos en España y poder realizar búsquedas utilizando las capacidades de VectorSearch he visto la oportunidad de realizar una pequeña prueba.
Datos para la prueba
La información relativa a los concursos públicos es proporcionada mensualmente en archivos de XML desde esta URL y el formato típico de la información de un concurso es tal que así:
Como veis, cada concurso tiene unas dimensiones considerables y en cada archivo podemos encontrar unos 450 concursos. Esta dimensión no hace viable usar una clase de ObjectScript para su mapeo (se podría...pero no estoy por la labor).
Códigos para las pruebas
Mi idea es capturar únicamente los campos relevantes para posteriores búsquedas, para ello he creado la siguiente clase que nos servirá para almacenar la información capturada:
Class Inquisidor.Object.Licitacion Extends (%Persistent, %XML.Adaptor) [ DdlAllowed ]
{
Property IdLicitacion As %String(MAXLEN = 200);
Property Titulo As %String(MAXLEN = 2000);
Property URL As %String(MAXLEN = 1000);
Property Resumen As %String(MAXLEN = 2000);
Property TituloVectorizado As %Vector(DATATYPE = "DECIMAL", LEN = 384);
Property Contratante As %String(MAXLEN = 2000);
Property URLContratante As %String(MAXLEN = 2000);
Property ValorEstimado As %Numeric(STORAGEDEFAULT = "columnar");
Property ImporteTotal As %Numeric(STORAGEDEFAULT = "columnar");
Property ImporteTotalSinImpuestos As %Numeric(STORAGEDEFAULT = "columnar");
Property FechaAdjudicacion As %Date;
Property Estado As %String;
Property Ganador As %String(MAXLEN = 200);
Property ImporteGanador As %Numeric(STORAGEDEFAULT = "columnar");
Property ImporteGanadorSinImpuestos As %Numeric(STORAGEDEFAULT = "columnar");
Property Clasificacion As %String(MAXLEN = 10);
Property Localizacion As %String(MAXLEN = 200);
Index IndexContratante On Contratante;
Index IndexGanador On Ganador;
Index IndexClasificacion On Clasificacion;
Index IndexLocalizacion On Localizacion;
Index IndexIdLicitation On IdLicitacion [ PrimaryKey ];
}
Para la captura de los datos mediante Embedded Python he utilizado la librería xml.etree.ElementTree que nos permite extraer los valores nodo a nodo. Aquí tenéis el método de Python que he usado para el mapeo del XML:
Method ReadXML(xmlPath As %String) As %String [ Language = python ]
{
import xml.etree.ElementTree as ET
import iris
import pandas as pd
try :
tree = ET.parse(xmlPath)
root = tree.getroot()
for entry in root.iter("{http://www.w3.org/2005/Atom}entry"):
licitacion = {"titulo": "", "resumen": "", "idlicitacion": "", "url": "", "contratante": "", "urlcontratante": "", "estado": "", "valorestimado": "", "importetotal": "", "importetotalsinimpuestos": "", "clasificacion": "", "localizacion": "", "fechaadjudicacion": "", "ganador": "", "importeganadorsinimpuestos": "", "importeganador": ""}
for tags in entry:
if tags.tag == "{http://www.w3.org/2005/Atom}title":
licitacion["titulo"] = tags.text
if tags.tag == "{http://www.w3.org/2005/Atom}summary":
licitacion["resumen"] = tags.text
if tags.tag == "{http://www.w3.org/2005/Atom}id":
licitacion["idlicitacion"] = tags.text
if tags.tag == "{http://www.w3.org/2005/Atom}link":
licitacion["url"] = tags.attrib["href"]
if tags.tag == "{urn:dgpe:names:draft:codice-place-ext:schema:xsd:CommonAggregateComponents-2}ContractFolderStatus":
for detailTags in tags:
if detailTags.tag == "{urn:dgpe:names:draft:codice-place-ext:schema:xsd:CommonAggregateComponents-2}LocatedContractingParty":
for infoContractor in detailTags:
if infoContractor.tag == "{urn:dgpe:names:draft:codice:schema:xsd:CommonAggregateComponents-2}Party":
for contractorDetails in infoContractor:
if contractorDetails.tag == "{urn:dgpe:names:draft:codice:schema:xsd:CommonAggregateComponents-2}PartyName" :
for name in contractorDetails:
licitacion["contratante"] = name.text
elif contractorDetails.tag == "{urn:dgpe:names:draft:codice:schema:xsd:CommonBasicComponents-2}WebsiteURI":
licitacion["urlcontratante"] = contractorDetails.text
elif detailTags.tag == "{urn:dgpe:names:draft:codice-place-ext:schema:xsd:CommonAggregateComponents-2}ContractFolderStatusCode":
licitacion["estado"] = detailTags.text
elif detailTags.tag == "{urn:dgpe:names:draft:codice:schema:xsd:CommonAggregateComponents-2}ProcurementProject":
for infoProcurement in detailTags:
if infoProcurement.tag == "{urn:dgpe:names:draft:codice:schema:xsd:CommonAggregateComponents-2}BudgetAmount":
for detailBudget in infoProcurement:
if detailBudget.tag == "{urn:dgpe:names:draft:codice:schema:xsd:CommonBasicComponents-2}EstimatedOverallContractAmount":
licitacion["valorestimado"] = detailBudget.text
elif detailBudget.tag == "{urn:dgpe:names:draft:codice:schema:xsd:CommonBasicComponents-2}TotalAmount":
licitacion["importetotal"] = detailBudget.text
elif detailBudget.tag == "{urn:dgpe:names:draft:codice:schema:xsd:CommonBasicComponents-2}TaxExclusiveAmount":
licitacion["importetotalsinimpuestos"] = detailBudget.text
elif infoProcurement.tag == "{urn:dgpe:names:draft:codice:schema:xsd:CommonAggregateComponents-2}RequiredCommodityClassification":
for detailClassification in infoProcurement:
if detailClassification.tag == "{urn:dgpe:names:draft:codice:schema:xsd:CommonBasicComponents-2}ItemClassificationCode":
licitacion["clasificacion"] = detailClassification.text
elif infoProcurement.tag == "{urn:dgpe:names:draft:codice:schema:xsd:CommonAggregateComponents-2}RealizedLocation":
for detailLocalization in infoProcurement:
if detailLocalization.tag == "{urn:dgpe:names:draft:codice:schema:xsd:CommonBasicComponents-2}CountrySubentity":
licitacion["localizacion"] = detailLocalization.text
elif detailTags.tag == "{urn:dgpe:names:draft:codice:schema:xsd:CommonAggregateComponents-2}TenderResult":
for infoResult in detailTags:
if infoResult.tag == "{urn:dgpe:names:draft:codice:schema:xsd:CommonBasicComponents-2}AwardDate":
licitacion["fechaadjudicacion"] = infoResult.text
elif infoResult.tag == "{urn:dgpe:names:draft:codice:schema:xsd:CommonAggregateComponents-2}WinningParty":
for detailWinner in infoResult:
if detailWinner.tag == "{urn:dgpe:names:draft:codice:schema:xsd:CommonAggregateComponents-2}PartyName":
for detailName in detailWinner:
if detailName.tag == "{urn:dgpe:names:draft:codice:schema:xsd:CommonBasicComponents-2}Name":
licitacion["ganador"] = detailName.text
elif infoResult.tag == "{urn:dgpe:names:draft:codice:schema:xsd:CommonAggregateComponents-2}AwardedTenderedProject":
for detailTender in infoResult:
if detailTender.tag == "{urn:dgpe:names:draft:codice:schema:xsd:CommonAggregateComponents-2}LegalMonetaryTotal":
for detailWinnerAmount in detailTender:
if detailWinnerAmount.tag == "{urn:dgpe:names:draft:codice:schema:xsd:CommonBasicComponents-2}TaxExclusiveAmount":
licitacion["importeganadorsinimpuestos"] = detailWinnerAmount.text
elif detailWinnerAmount.tag == "{urn:dgpe:names:draft:codice:schema:xsd:CommonBasicComponents-2}PayableAmount":
licitacion["importeganador"] = detailWinnerAmount.text
iris.cls("Ens.Util.Log").LogInfo("Inquisidor.BP.XMLToLicitacion", "VectorizePatient", "Terminado mapeo "+licitacion["titulo"])
if licitacion.get("importeganador") is not None and licitacion.get("importeganador") is not "":
iris.cls("Ens.Util.Log").LogInfo("Inquisidor.BP.XMLToLicitacion", "VectorizePatient", "Lanzando insert "+licitacion["titulo"])
stmt = iris.sql.prepare("INSERT INTO INQUISIDOR_Object.Licitacion (Titulo, Resumen, IdLicitacion, URL, Contratante, URLContratante, Estado, ValorEstimado, ImporteTotal, ImporteTotalSinImpuestos, Clasificacion, Localizacion, FechaAdjudicacion, Ganador, ImporteGanadorSinImpuestos, ImporteGanador) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,TO_DATE(?,'YYYY-MM-DD'),?,?,?)")
try:
rs = stmt.execute(licitacion["titulo"], licitacion["resumen"], licitacion["idlicitacion"], licitacion["url"], licitacion["contratante"], licitacion["urlcontratante"], licitacion["estado"], licitacion["valorestimado"], licitacion["importetotal"], licitacion["importetotalsinimpuestos"], licitacion["clasificacion"], licitacion["localizacion"], licitacion["fechaadjudicacion"], licitacion["ganador"], licitacion["importeganadorsinimpuestos"], licitacion["importeganador"])
except Exception as err:
iris.cls("Ens.Util.Log").LogInfo("Inquisidor.BP.XMLToLicitacion", "VectorizePatient", repr(err))
return "Success"
except Exception as err:
iris.cls("Ens.Util.Log").LogInfo("Inquisidor.BP.XMLToLicitacion", "VectorizePatient", repr(err))
return "Error"
}
Una vez finalizado el mapeo procedemos a realizar un simple insert con el registro.
Para el mapeo usando ObjectScript he usado la funcionalidad %XML.TextReader, veamos el método:
Method OnRequest(pRequest As Ens.StreamContainer, Output pResponse As Ens.Response) As %Status
{
set filename = pRequest.OriginalFilename
set status=##class(%XML.TextReader).ParseFile(filename,.textreader)
//check status
if $$$ISERR(status) {do $System.Status.DisplayError(status) quit}
set tStatement = ##class(%SQL.Statement).%New()
//iterate through document, node by node
while textreader.Read()
{
if ((textreader.NodeType = "element") && (textreader.Depth = 2) && (textreader.Path = "/feed/entry")) {
if ($DATA(licitacion))
{
if (licitacion.ImporteGanador '= ""){
//set sc = licitacion.%Save()
set myquery = "INSERT INTO INQUISIDOR_Object.LicitacionOS (Titulo, Resumen, IdLicitacion, URL, Contratante, URLContratante, Estado, ValorEstimado, ImporteTotal, ImporteTotalSinImpuestos, Clasificacion, Localizacion, FechaAdjudicacion, Ganador, ImporteGanadorSinImpuestos, ImporteGanador) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
set qStatus = tStatement.%Prepare(myquery)
if qStatus '= 1 {
write "%Prepare failed:" do $System.Status.DisplayError(qStatus)
quit
}
set rset = tStatement.%Execute(licitacion.Titulo, licitacion.Resumen, licitacion.IdLicitacion, licitacion.URL, licitacion.Contratante, licitacion.URLContratante, licitacion.Estado, licitacion.ValorEstimado, licitacion.ImporteTotal, licitacion.ImporteTotalSinImpuestos, licitacion.Clasificacion, licitacion.Localizacion, licitacion.FechaAdjudicacion, licitacion.Ganador, licitacion.ImporteGanadorSinImpuestos, licitacion.ImporteGanador)
}
}
set licitacion = ##class(Inquisidor.Object.LicitacionOS).%New()
}
if (textreader.Path = "/feed/entry/title"){
if (textreader.Value '= ""){
set licitacion.Titulo = textreader.Value
}
}
if (textreader.Path = "/feed/entry/summary"){
if (textreader.Value '= ""){
set licitacion.Resumen = textreader.Value
}
}
if (textreader.Path = "/feed/entry/id"){
if (textreader.Value '= ""){
set licitacion.IdLicitacion = textreader.Value
}
}
if (textreader.Path = "/feed/entry/link"){
if (textreader.MoveToAttributeName("href")) {
set licitacion.URL = textreader.Value
}
}
if (textreader.Path = "/feed/entry/cac-place-ext:ContractFolderStatus/cbc-place-ext:ContractFolderStatusCode"){
if (textreader.Value '= ""){
set licitacion.Estado = textreader.Value
}
}
if (textreader.Path = "/feed/entry/cac-place-ext:ContractFolderStatus/cac-place-ext:LocatedContractingParty/cac:Party/cac:PartyName"){
if (textreader.Value '= ""){
set licitacion.Contratante = textreader.Value
}
}
if (textreader.Path = "/feed/entry/cac-place-ext:ContractFolderStatus/cac-place-ext:LocatedContractingParty/cac:Party/cbc:WebsiteURI"){
if (textreader.Value '= ""){
set licitacion.URLContratante = textreader.Value
}
}
if (textreader.Path = "/feed/entry/cac-place-ext:ContractFolderStatus/cac:ProcurementProject/cac:BudgetAmount/cbc:EstimatedOverallContractAmount"){
if (textreader.Value '= ""){
set licitacion.ValorEstimado = textreader.Value
}
}
if (textreader.Path = "/feed/entry/cac-place-ext:ContractFolderStatus/cac:ProcurementProject/cac:BudgetAmount/cbc:TotalAmount"){
if (textreader.Value '= ""){
set licitacion.ImporteTotal = textreader.Value
}
}
if (textreader.Path = "/feed/entry/cac-place-ext:ContractFolderStatus/cac:ProcurementProject/cac:BudgetAmount/cbc:TaxExclusiveAmount"){
if (textreader.Value '= ""){
set licitacion.ImporteTotalSinImpuestos = textreader.Value
}
}
if (textreader.Path = "/feed/entry/cac-place-ext:ContractFolderStatus/cac:ProcurementProject/cac:RequiredCommodityClassification/cbc:ItemClassificationCode"){
if (textreader.Value '= ""){
set licitacion.Clasificacion = textreader.Value
}
}
if (textreader.Path = "/feed/entry/cac-place-ext:ContractFolderStatus/cac:ProcurementProject/cac:RealizedLocation/cbc:CountrySubentity"){
if (textreader.Value '= ""){
set licitacion.Localizacion = textreader.Value
}
}
if (textreader.Path = "/feed/entry/cac-place-ext:ContractFolderStatus/cac:TenderResult/cbc:AwardDate"){
if (textreader.Value '= ""){
set licitacion.FechaAdjudicacion = $System.SQL.Functions.TODATE(textreader.Value,"YYYY-MM-DD")
}
}
if (textreader.Path = "/feed/entry/cac-place-ext:ContractFolderStatus/cac:TenderResult/cac:WinningParty/cac:PartyName/cbc:Name"){
if (textreader.Value '= ""){
set licitacion.Ganador = textreader.Value
}
}
if (textreader.Path = "/feed/entry/cac-place-ext:ContractFolderStatus/cac:TenderResult/cac:AwardedTenderedProject/cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount"){
if (textreader.Value '= ""){
set licitacion.ImporteGanadorSinImpuestos = textreader.Value
}
}
if (textreader.Path = "/feed/entry/cac-place-ext:ContractFolderStatus/cac:TenderResult/cac:AwardedTenderedProject/cac:LegalMonetaryTotal/cbc:PayableAmount"){
if (textreader.Value '= ""){
set licitacion.ImporteGanador = textreader.Value
}
}
}
// set resultEmbeddings = ..GenerateEmbeddings()
Quit $$$OK
}
Ambos códigos registrarán en la base de datos únicamente aquellos concursos que hayan sido ya resueltos (tienen informado el importe total ganador).
Configuración de la producción
Con nuestros métodos implementados en los correspondientes Business Process sólo nos queda para nuestra prueba configurar la producción que nos va a permitir alimentar ambos métodos. Simplemente añadiremos dos Business Service que se limitarán a capturar los archivos con la información XML y remitírsela a los Business Process.
Crearemos dos Business Service para evitar cualquier posible interferencia a la hora de capturar y remitir la información a los Business Process. La producción tendrá la siguiente pinta:
Para la prueba introduciremos los concursos públicos correspondientes al mes de Febrero que hacen un total de 91 ficheros con 1.30 GB de datos. Veamos como se portan ambos códigos.
Preparados...
En sus marcas...
Ya!
Resultados de mapeo XML en ObjectScript
Empecemos con el tiempo que le ha tomado al código de ObjectScript el mapeo de los 91 ficheros:
El primer fichero comenzó a las 21:11:15, veamos cuando se mapeo el último fichero:
Si vemos los detalles del último mensaje podremos ver la fecha de finalización del procesamiento:
La hora de finalización es las 21:17:43, eso hace un tiempo de tratamiento de 6 minutos y 28 segundos.
Resultados de mapeo XML en Embedded Python
Repitamos la misma operación con el proceso que usa Python:
Comienzo a las 21:11:15 como en el caso anterior, veamos cuando finalizó:
Veamos el mensaje en detalle para conocer el final exacto:
La hora de finalización fue las 21:12:03, lo que nos lleva a un total de 48 segundos.
¡Pues tenemos ganador! En este round Embedded Python ha batido a ObjectScript al menos en lo que respecta al parseo de XML. Si tenéis alguna sugerencia o mejora en el código de ambos métodos os animo a que lo pongáis en los comentarios y repetiré las pruebas para comprobar las posibles mejoras.
Lo que si podemos afirmar es que respecto a la superioridad manifiesta de rendimiento de ObjectScript respecto a Python...