Understanding records in Koha

Throughout the years, I’ve found several open source ILS and most of them try to water down the way librarians have catalogued resources for years. Yes, we all agree ISO 2709 is obsolete, but MARC has proven to be very complete, and most of the efforts out there (Dublin Core, etc.) try to reduce the expression level a librarian can have. If your beef is with ISO 2709, there’s MARC-XML if you want something that is easier to debug in terms of encoding, etc.

That said, Koha faces a challenge: it needs to balance the expressiveness of MARC with the rigidness of SQL. It also needs to balance the convenience of SQL with the potential shortcomings of their database of choice (MySQL) with large collections (over a couple thousand records) and particularly with searching and indexing.

Koha’s approach to solve this problem is to incorporate Zebra to the mix. Zebra is a very elegant, but very difficult to understand piece of Danish open source software that is very good at indexing and searching resources that can come from, say, MARC. It runs as a separate process (not part of the Web stack) and it can also be enabled as a Z39.50 server (Koha itself is a Z39.50 consumer, courtesy of Perl)

The purpose of this post is to help readers navigate how records are managed in Koha and avoid frustrations when deploying Koha instances and migrating existing records.

Koha has a very simple workflow for cataloguing new resources, either from Z39.50, from a MARC (ISO 2709 or XML) file or from scratch. It has templates for cataloguing, it has the Z39.50 and MARC capabilities, and it has authorities. The use case of starting a library from scratch in Koha is actually a very solid one.

But all of the libraries I’ve worked with in the last 7 years already have a collection. This collection might be ISIS, Documanager, another SQL database or even a spreadsheet. Few of them have MARC files, and even if they had (i.e., vendors provide them), they still want ETLs to be applied (normalization, Z39.50 validations, etc.) that require processing.

So, how do we incorporate records massively into Koha? There are two methods, MARC import or fiddling with SQL directly, but only one answer: MARC import.

See, MARC can potentially have hundreds of fields and subfields, and we don’t necessarily know beforehand which ones are catalogued by the librarians, by other libraries’ librarians or even by the publisher. Trying to water it down by removing the fields we don’t “want” is simply denying a full fidelity experience for patrons.

But, in the other hand, MySQL is not designed to accommodate a random, variable number of columns. So Koha takes the most used attributes (like title or author) and “burns” them into SQL. For multivalued attributes, like subjects or items, it uses additional tables. And then it takes the MARC-XML and shoves it on a entire field.

Whoa. So what happens if a conservatorium is making heavy use of 383b (Opus number) and then want to search massively for this field/subfield combination? Well, you can’t just tell Koha to wait until MySQL loads all the XMLs in memory, blows them up and traverse them – it’s just not gonna happen within timeout.

At this point you must have figured out that the obvious solution is to drop the SQL database and go with a document-oriented database. If someone just wants to catalog 14 field/subfields and eventually a super detailed librarian comes in and starts doing 150, you would be fine.

Because right now, without that, it’s Zebra that kicks in. It behaves more like an object storage and it’s very good at searching and indexing (and it serves as Z39.50 server, which is nice) but it’s a process running separately and management can sometimes be harsh.

Earlier we discussed the use case where Koha excels: creating records from scratch. Does this mean that Koha won’t work for an existing collection? No. It just means the workflows are a tad more complicated.

I write my own Perl code to migrate records (some scripts available here, on the move to GitHub), and the output is always MARC. In the past I’ve done ISO 2709, yes, but I only do MARC-XML now. Although it can potentially use up more disk space, and it could be a bit more slow to load, it has a quick benefit for us non-English speakers: it allows to solve encoding issues faster (with the binary, I had to do hexadecimal sed’s and other weird things and it messed up with headers, etc.)

Sometimes I do one record per file (depending on the I/O reality I have to face) but you can do several at a time: a “collection” in just one file, that tends to use up more RAM but also makes it more difficult to pinpoint and solve problems with specific records. I use the bulkmarcimport tool. I make sure the holdings (field 942 in Koha unless you change it) are there before loading, otherwise I really mess up the DB. And my trial/error process usually involves using mysql’s dump and restore facilities and removing the content of the /var/lib/koha/zebradb directory, effectively starting from scratch.

Koha requires indexing, and it can be very frustrating to learn that after you import all your records, you still can’t find anything on the OPAC. Most distro packages for Koha have a helper script called koha-rebuild-zebra which helps you in the process. Actually, in my experience deploying large Koha installations, most of the management and operational issues have something to do with indexing. APT packages for Koha will install a cron task to rebuild Zebra, pointing at the extreme importance (dependency) on this process.

Since Koha now works with instance names (a combination of Zebra installations, MySQL databases and template files) you can rebuild using something like:

koha-rebuild-zebra -b -v -f mybiblio

Feel free to review how that script works and what other (Perl) scripts it calls. It’s fun and useful to understand how old pieces of Koha fit a generally new paradigm. That said, it’s time to embrace cloud patterns and practices for open source ILS – imagine using a bus topic for selective information dissemination or circulation, and an abstract document-oriented cloud storage for the catalogue, with extensive object caching for searches. And to do it all without VMs, which are usually a management nightmare for understaffed libraries.

Paralelizando clientes para pruebas de estrés en aplicaciones Web

El pasado 4 de junio tuve la oportunidad de compartir con la gente de Iguana Valley una presentación nivel 100 sobre balanceo de carga y alta disponibilidad en aplicaciones Web, con motivo del último RefreshUIO, un evento mensual de emprendimientos y tecnologías Web que se hace en la ciudad de Quito.

Además de presentar algunos escenarios de clusterización para balanceo de carga y/o alta disponibilidad, que ya he expuesto y documentado en el blog y en los artículos de mi Scribd previamente, y algunos nuevos escenarios interoperables que estaré presentando como parte de mi trabajo en los próximos días, algunas personas me preguntaron sobre el script que utilicé para generar carga sobre el cluster y demostrar el balanceo de carga que realizaba IPVS (LVS) en Linux.

Este script está en realidad basado en el concepto que desarrollé para las pruebas de stress de LDAP, utilizando Perl con threads. Recomiendo que lean ese artículo antes de utilizar este script. El código del script sigue a continuación, y lo único que deben indicar es el número de threads que deben ejecutarse y la URI a solicitar. El script utiliza LWP, y bota en STDOUT la salida decodificada de la ejecución, por lo que quizás quiera pasar la salida por grep o enviarla a algún otro sitio.

## plt-bambam.pl -- Web Server Stress Testing Adaptation#   (C) 2008-2011 José Miguel Parrella Romero ## This is free software, released under the terms of Perl itself.#use strict;use LWP::UserAgent;use threads;use threads::shared;$|=1;my @threads;## CONFIGURATION#my $uri = 'http://192.168.56.100/';my $count = 200;## END OF CONFIGURATION#my $ua = LWP::UserAgent->new;while ( $count ) {  push(@threads, threads->new(&query));  --$count;}foreach my $thread (@threads) {  $thread->join();}sub query {  my $res = $ua->get($uri);  print $res->decoded_content;}

Stalking IPv6 adoption in LatAm govements

Motivado por una reciente discusión en la lista de LACNOG, promovida por Carlos Martínez de Uruguay, escribí un pedazo de código en Perl que permitiera cosechar una lista de dominios de gobieo (gov.cc, gob.cc) sobre los cuales consultar un conjunto de prefijos que pudieran potencialmente reflejar el despliegue de uno o más servidores IPv6 de cara a Inteet.

Intenté realizar las búsquedas utilizando Google, pero las API de búsqueda de este servicio parecieran no ayudar devolviendo un número útil de resultados. Estoy usando Yahoo! aunque al no tener una aplicación registrada la API solo devuelve 1000 resultados. Mejor que nada.

Este script (unique-domain-builder) en Perl realiza una consulta genérica en el buscador y utiliza un módulo de Perl para obtener el dominio base de la URL retoada. El programa se encarga de manejar los casos repetidos, y de sacar en STDOUT una lista de dominios con los potenciales prefijos ya añadidos, como ipv6, www6, entre otros.

Pude haber agregado al programa en Perl alguna librería de resolución DNS para verificar la resolución de registros AAAA para estos dominios y subdominios generados, pero decidí por ahora reusar la popular herramienta host de Unix para la verificación, basta con correr host $dominio y hacer un grep por IPv6.

¿Resultados? Luego de buscar en 1666 registros para Venezuela, 2765 para Ecuador, 3871 para Argentina y 2009 para Brasil, solo obtengo resultados para corpoelec.gob.ve (donde, por cierto, trabajé hace unos años y desplegamos varios servicios en IPv6)

¿Inconvenientes? Muchos. En primer lugar, este no debería ser el mejor mecanismo para buscar información sobre la adopción de IPv6. Además, esto solo mide adopción, y no despliegue o aprovechamiento de IPv6 para algo más que alcanzar un hito. Para leer más rants, puedes unirte a la lista de LACNOG.

Koha with no barcodes

Traditionally, Koha 3 depends on the items (we call them existencias in spanish) having a barcode in order to uniquely identify each item. Circulation, for example, requires the librarian to scan the barcode of an item in order to circulate it.At times, this proves inconvenient since lots of biblios (titles, or títulos in spanish) have the same barcode printed on each item (usually the ISBN number) forcing the library to print new unique barcodes (Koha has a nice barcode generator) for each one of the items in existence.However, it’s usually not feasible to relabel all items with new barcodes, especially if you have millions of items nationwide. So, I thought of an easy patch to Koha that allows to circulate items based on the item number, and not the barcode.First of all, you should set the barcode number for each item equal to the item number for those items where you don’t have any barcode recorded. These is best accomplished after loading MARC records on the database using the MySQL console:

UPDATE items SET barcode = itemnumber; -- optionally using something like WHERE barcode = ''

On my case, for over 1.1 million items, it took some 3 minutes 6 seconds to complete. There’s a drawback, however, because you need to run this periodically as you add more items, but it’s not something your DBA can’t automate. At this point you can circulate items using items number, and you can print barcodes with that number, but it’s still not easy for the librarian to either remember the item number or look it up before circulating.You can apply an easy patch on line 44 of the modules/catalogue/moredetail.tmpl file of the Intranet, providing a new link on the Items tab of a biblio to start the borrowing workflow for a specific item:

<!-- TMPL_UNLESS NAME="issue" -->[Circulate item ]<!-- /TMPL_UNLESS -->

Of course, circ/circulation.pl on the Intranet also needs a small patch to store the barcode number on the session and then reusing it when the borrower is selected, near line 111:

my $barcode;if ( $session->param('barcode') ) {  $barcode = $session->param('barcode');  $session->clear('barcode');} elsif ( $query->param('barcode') ) {  $barcode = $query->param('barcode') || '';  $session->param('barcode', $barcode);}$barcode =~  s/^s*|s*$//g; # remove leading/trailing whitespace...

Restart your Web server and that’s it. You can now search for a biblio, go to the Items tab, select an item to be circulated, select a borrower, and the item is circulated. For retus, search for the user and go to the end of the page, you can see all items on circulation, fines and retu options. The workflow changes a little bit, but it’s the easiest way I’ve devised to operate a Koha ILS when barcodes are absent or outside your control.

Stressting LDAP for fun and profit

Hace casi dos años, el grupo de Software Libre de PDVSA, la petrolera estatal venezolana, estaba indeciso sobre la utilización de OpenLDAP como solución de directorio compatible con LDAPv3 que les permitiera sustituir parcial o totalmente Microsoft Active Directory. Ellos/as me plantearon su mayor duda: ¿qué tan robusto es OpenLDAP en entoos empresariales?

Yo ya había utilizado OpenLDAP extensivamente en entoos empresariales, incluso en convivencia con Microsoft Active Directory cuando trabajé en el Centro de Cómputo de EDELCA, la principal empresa eléctrica en Venezuela. Ese setup ahora está utilizando 389 (antes Fedora Directory Server) para sincronizar claves con MSAD para más de 17 mil personas y centenares de miles de objetos, y a pesar de que lo había visto funcionar bajo cargas muy inusuales, y usábamos clusters con distribución geográfica, yo también tenía la duda de los números.

Decidí probar esto en varios frentes. El primero, generar LDIFs muy grandes, y averiguar cuánto representarían en disco una vez almacenados en Berkeley DB, suponiendo que se fuera a utilizar ese popular backend para OpenLDAP. Esto nos daría una idea, exceptuando índices y optimizaciones, de los requerimientos en memoria de un producto como OpenLDAP en escenarios muy grandes. El segundo frente estaba limitado a las pruebas de tiempo de respuesta de la aplicación ante escenarios de búsqueda.

La razón por la que pruebo búsquedas es porque esta es la aplicación más común de un directorio LDAP. Mis pruebas fueron locales usando TCP/IP porque, aparte de contaminar los tiempos, en mi experiencia los tiempos de respuesta de un directorio LDAP, cuando está networked, se diluyen en los tiempos de procesamiento de la aplicación; en EDELCA preparé un informe que estudiaba la razón de un problema con GNOME Evolution en el cual el autocompletado tardaba varios segundos en responder, aunque en trazas de red se evidenciaba que el directorio LDAP había respondido en milisegundos (un bug en la aplicación) Imagine otro escenario: en la firma y la comprobación de firma de una infraestructura de llave pública dependiente de LDAP, ¿será el tiempo de respuesta de LDAP determinante cuando estamos potencialmente sobrecargando al cliente calculando hashes para la firma de un documento?

Por esto, el tiempo de respuesta local vs. networked no sería un indicativo de la experiencia del usuario final con el directorio, en parte el motivo de la preocupación del cliente. Además hay proxys en LDAP (montamos uno en PDVSA CRP, junto con Penrose como integrador de directorios, o como lo llaman en el mercado, un virtual directory) y estrategias de infraestructura para atacar esto de manera estructural.

Como no quería pasar seis meses de mi vida diseñando experimentos y ya que supuse que nadie iba a estar demasiado interesado en los resultados, dejé de lado algunos escenarios de borde y preparé un par de scripts, en Perl como es natural, para escribir LDIFs muy rápido y para consultar LDAPs muy rápido. Utilicé threads en Perl para su implementación (quería consultar en paralelo), aunque estoy prácticamente convencido por Luis Muñoz, Alejandro Imass y Eesto Heández-Novich de no repetir el uso de threads jamás.

De allí nació un pedestrian LDAP tester, que luego extendí a un stresster de MTAs a través de SMTP y a uno de procesos de autenticación en aplicaciones Web utilizando WWW::Mechanize. Esto es porque el proyecto con PDVSA involucraba las tres cosas: implementamos un cluster de Zimbra Collaboration Suite Community y 389 (FDS) utilizando Debian GNU/Linux en ocho servidores con Xen y NFSv3 para un poco más de 25 mil cuentas, a la fecha 18 meses en producción. Voy a subir los otros scripts pronto, pero vamos con los findings de LDAP, de mi nota informal de Julio 2008:

Cargar 200 mil registros (un LDIF de 66 MB.) en la base de datos tiene una representación en BDB de unos 928 MB. Asumamos que requeriríamos, worst case scenario ese espacio en memoria, sin índices ni los registros de replicación. Cargar esos 200 mil registros en OpenLDAP (slapadd) se toma entre 135 y 180 minutos aproximadamente. Afortunadamente la carga se hace sólo una vez. Indizar la base de datos (slapindex) es relativamente rápido y el tiempo se mantiene igual con 1 mil o 200 mil registros. Esto es lógico y bueno.

Hice una búsqueda similar a una consulta de una libreta de direcciones popular. Esa búsqueda generó 15 mil resultados (el número de resultados se puede restringir en OpenLDAP, pero a efectos de la prueba lo desactivé) — en mis pruebas, 100 clientes consultando paralelamente por TCP/IP y obteniendo los 15 mil resultados, sin limitar los atributos más allá de las ACLs se tomó:

real    0m12.048suser    0m10.413ssys     0m0.768s

Lo cual es más que aceptable. Ahora bien, el uso en memoria RAM del servicio está en los 640 MB. de memoria virtual y casi 200 MB. en memoria residente. Esto representa el 15% de la memoria del servidor que utilicé, un HP Integrity descontinuado para 2008, que tiene 2 GB. de RAM. Al introducir índices, estos se cargan en memoria, así como el número de objetos que se quiere tener en caché (yo coloqué 50 mil) por lo que habría que pensar en al menos unos 4 GB. de RAM para un volumen así.

Creando índices para los atributos cn, sn, givenName y mail, y ajustando un poco los parámetros de configuración de BerkeleyDB (tabulado del FAQ-O-Matic) la indización se toma un tiempo bastante mayor, para un tiempo de respuesta con la misma búsqueda no mucho menor:

real    0m10.540suser    0m8.593ssys     0m0.480s

Por lo que, en general, optimizar poco los índices tendría mucho impacto en las cargas y poco impacto en las búsquedas: si vas a indizar, indiza todo lo que quieras y puedas. Lo otro crítico de optimizar es el DBCONFIG; para 2008, y gracias en parte al oscurantismo de Oracle, era más arte que ciencia. El FAQ-O-Matic de OpenLDAP, como siempre, da la mejor información, ver los artículos 1072, 1075, 42, 738 y 893.

Now for the goodies, hay tres scripts que publiqué en mi carpeta de CPAN. El primero genera LDIFs, de forma muy básica, entradas tipo libreta de direcciones (plt-ldif-generator); el segundo hace las consultas con threads (plt-bambam) y el tercero es una muestra del patrón de uso del tester (plt-usage-patte), en shell script.

Necesita un Perl razonablemente modeo, compilado con soporte para threads, la librería de Threads de Perl y el módulo Net::LDAP. Un aptitude install libthreads-perl libnet-ldap-perl es suficiente. El primer script también espera que usted provea un root.ldif (el nombre se puede cambiar en el código) con la entrada base del directorio (dc=foo,dc=bar; por ejemplo) y un model.ldif que tenga la entrada modelo con tags que el script sustituirá (obvio, puede usar un LDIF prehecho), algo como:

dn: cn=#CN#,dc=foo,dc=barobjectClass: topobjectClass: inetorgpersonuid: #USER#cn: #CN#sn: #SN#givenName: #FN#

Importante: como ya dije antes, algunos casos de uso están fuera de la prueba que realicé, en la mayoría de los casos porque no impactan la prueba
sensiblemente (por ejemplo, autenticar las conexiones usando credenciales, cifrado con TLS/SSL y otros, no sería un problema grave) aunque sí podría nombrar algunos elementos que influyen en el performance cuando se lleva OpenLDAP a la práctica en escenarios distintos al trivial, como por ejemplo el uso de muchos schemas y de atributos divergentes en las entradas del directorio, sobrecargar el servicio con temas administrativos como largas ACLs dinámicas y sobre todo altos niveles de verbosidad y overlays de terceros. Afortunadamente, ninguno de estos es un must para un setup grande de LDAP. El código de mis scripts también debe tener bugs y está terriblemente documentado, pero le cumple el propósito perfectamente.