ZOOM(3pm)

๐Ÿ‘‘ ZOOM(3pm) โ€” the Perl extension implementing the ZOOM API for Information Retrieval ๐Ÿ’–

okay bestie, welcome to the ZOOM(3pm) page โ€” it lives in the User Contributed Perl Documentation corner of your manual, and it's about to be your whole personality. grab a strawberry drink ๐Ÿ“, we're doing information retrieval.

โœจ NAME โœจ

ZOOM โ€” a Perl extension implementing the ZOOM API for Information Retrieval.

two words, one vibe: it's giving "search literally everything" ๐Ÿ’…

๐Ÿฆ‹ SYNOPSIS ๐Ÿฆ‹

use ZOOM;
eval {
    $conn = new ZOOM::Connection($host, $port,
                                 databaseName => "mydb");
    $conn->option(preferredRecordSyntax => "usmarc");
    $rs = $conn->search_pqf('@attr 1=4 dinosaur');
    $n = $rs->size();
    print $rs->record(0)->render();
};
if ($@) {
    print "Error ", $@->code(), ": ", $@->message(), "\n";
}

see that? that's the whole plot of your life right there. connect, set an option, search for dinosaurs, count the results, render one, and catch the drama if something pops off. ๐Ÿฆ•


๐ŸŒธ DESCRIPTION ๐ŸŒธ

So this module gives you a nice, Perlish implementation of the ZOOM Abstract API, which is described and documented over at http://zoom.z3950.org/api/.

Under the hood, no cap: ZOOM is implemented as a set of thin classes sitting on top of the non-OO functions that this distribution's Net::Z3950::ZOOM module provides, which is itself a thin layer on top of the ZOOM-C code shipped as part of Index Data's YAZ Toolkit. And because ZOOM-C is also the code underneath the ZOOM bindings in C++, Visual Basic, Scheme, Ruby, .NET (including C#) and other languages, this Perl module works compatibly with all those other implementations. (Of course, the whole point of a public API like ZOOM is that all implementations should be compatible anyway; but knowing the same code is running underneath is lowkey comforting. ๐Ÿซถ)

Here's the inventory, bestie, don't skip it:

  • two enumerations: ZOOM::Error and ZOOM::Event
  • three utility functions living in the ZOOM package itself: diag_str(), event_str() and event()
  • eight classes: ZOOM::Exception, ZOOM::Options, ZOOM::Connection, ZOOM::Query, ZOOM::ResultSet, ZOOM::Record, ZOOM::ScanSet and ZOOM::Package

Of those eight, the Query class is abstract, and it has four concrete subclasses: ZOOM::Query::CQL, ZOOM::Query::PQF, ZOOM::Query::CQL2RPN and ZOOM::Query::CCL2RPN. And finally โ€” wait for it โ€” there's also a ZOOM::Query::Log module that supplies a genuinely useful general-purpose logging facility. ๐ŸŽ€

Many perfectly useful ZOOM applications can be built using only the Connection, ResultSet, Record and Exception classes, exactly like the code snippet up in the synopsis. iconic behaviour honestly.

A typical application starts by creating a Connection object, then uses that to run searches that give you ResultSet objects, then fetches records out of those result-sets to give you Record objects. And if something goes wrong, an Exception object gets thrown and you get to deal with it. โœจ

More sophisticated applications might also browse the server's indexes to make a ScanSet, and pull indexed terms out of it; others might send "Extended Services" Packages to the server to pull off non-standard tasks like database creation and record update. Want to search with a query syntax that isn't PQF? Use a query object from one of the Query subclasses. And finally, you can mess with sets of options independently of the objects they're attached to, using an Options object.

Generally? Method calls throw an exception if anything goes wrong, so you do not need to test for success after every single call. no vibes-checking every line, we love that. See the section below on the Exception class for the details.


๐Ÿ’œ UTILITY FUNCTIONS ๐Ÿ’œ

๐Ÿงš ZOOM::diag_str()

$msg = ZOOM::diag_str(ZOOM::Error::INVALID_QUERY);

Hands you back a human-readable, English-language string that matches the error code it was passed as its very own parameter. And it works for any error code you get out of ZOOM::Exception::code(), ZOOM::Connection::error_x() or ZOOM::Connection::errcode() โ€” doesn't matter whether it's a member of the ZOOM::Error enumeration or something pulled from the BIB-1 diagnostic set. slug, it handles both. ๐Ÿ’…

๐ŸŒˆ ZOOM::diag_srw_str()

$msg = ZOOM::diag_srw_str(18);

Returns a human-readable English-language string for the SRW error code you specify.

โญ ZOOM::event_str()

$msg = ZOOM::event_str(ZOOM::Event::RECV_APDU);

Returns a human-readable English-language string for the event code passed in as its own parameter. Works for any value of the ZOOM::Event enumeration. ๐Ÿฆ‹

๐Ÿ”ฅ ZOOM::event()

$connsRef = [ $conn1, $conn2, $conn3 ];
$which = ZOOM::event($connsRef);
$ev = $connsRef->[$which-1]->last_event()
    if ($which != 0);

For the complex asynchronous applications only, bestie โ€” this is main-character energy for multitaskers. This function takes a reference to a list of Connection objects, waits until an event happens on any one of them, and returns an integer telling you which of the connections it happened on. That return value is a 1-based index into the list; a 0 comes back if no event happens within the longest timeout specified by the timeout options of all the connections. ๐Ÿ˜ดโฐ

See the section below on asynchronous applications.


๐ŸŒŸ CLASSES ๐ŸŒŸ

The eight ZOOM classes get described here in "sensible order": first, the four commonly used classes, in the order that they'll tend to be used in most programs โ€” that's Connection, ResultSet, Record, Exception โ€” then the four more esoteric classes in descending order of how often you'll actually need them.

And with the exception of the Options class (which is an extension to the ZOOM model), the intro to each class includes a link to the relevant section of the ZOOM Abstract API. ๐Ÿ”—

๐Ÿ’– ZOOM::Connection

$conn = new ZOOM::Connection("indexdata.dk:210/gils");
print("server is '", $conn->option("serverImplementationName"), "'\n");
$conn->option(preferredRecordSyntax => "usmarc");
$rs = $conn->search_pqf('@attr 1=4 mineral');
$ss = $conn->scan('@attr 1=1003 a');
if ($conn->errcode() != 0) {
   die("somthing went wrong: " . $conn->errmsg())
}
$conn->destroy()

This class is a connection to an information retrieval server, speaking an IR protocol like ANSI/NISO Z39.50, SRW (the Search/Retrieve Webservice), SRU (the Search/Retrieve URL) or OpenSearch. Not all of these protocols actually need a low-level connection kept alive โ€” but the Connection object is still the spot where the necessary cache of configuration and state info hangs out, plus it gives you a uniform API to the connection-oriented facilities (searching, index browsing, etc.) that these protocols provide. ๐Ÿซถ

See the description of the "Connection" class in the ZOOM Abstract API at http://zoom.z3950.org/api/zoom-current.html#3.2

๐ŸŽ€ Methods

new()

$conn = new ZOOM::Connection("indexdata.dk", 210);
$conn = new ZOOM::Connection("indexdata.dk:210/gils");
$conn = new ZOOM::Connection("tcp:indexdata.dk:210/gils");
$conn = new ZOOM::Connection("http:indexdata.dk:210/gils");
$conn = new ZOOM::Connection("indexdata.dk", 210,
                              databaseName => "mydb",
                              preferredRecordSyntax => "marc");

Makes a brand-new Connection object and immediately connects it to the specified server. no waiting around, it's going straight for it. If you want a new Connection object but you're not ready to forge the connection yet, use the create() and connect() methods instead (they're right below, keep scrolling ๐Ÿ’…).

This constructor can be called with two arguments or a single argument. In the former case the arguments are the name and port number of the Z39.50 server to connect to; in the latter case, that single argument is a YAZ service-specifier string of the form

When the two-option form is used (which may be done using a vacuous second argument of zero), any number of additional argument pairs may be provided, and they're interpreted as key-value pairs to be set as options after the Connection object is created but before it connects to the server. That's the convenient way to set options โ€” including the ones that have to be set before connecting, like authentication tokens. ๐Ÿ”

The server-name string is of the form:

  • [scheme:]host[:port][/databaseName]

In which the host and port parts are as in the two-argument form, the databaseName (if you provide one) specifies the name of the database to be used in subsequent searches on this connection, and the optional scheme (default tcp) says which protocol should be used. At present, these schemes are supported:

  • tcp โ€” Z39.50 connection.
  • ssl โ€” Z39.50 connection encrypted using SSL (Secure Sockets Layer). Not many servers support this, but Index Data's Zebra is one that does.
  • unix โ€” Z39.50 connection on a Unix-domain (local) socket, in which case the hostname portion of the string is instead used as a filename in the local filesystem.
  • http โ€” SRU connection over HTTP.

If you're using the http scheme, the particular SRU flavour can be picked with the sru option, which takes these values:

  • soap โ€” SRU over SOAP (i.e. what used to be called SRW). This is the default.
  • get โ€” "SRU Classic" (i.e. SRU over HTTP GET).
  • post โ€” SRU over HTTP POST.

If an error occurs, an exception is thrown. That could mean a networking problem (like the host not being found or being unreachable), or a protocol-level problem (like a Z39.50 server rejecting the Init request). so rude of it. ๐Ÿ™ˆ

create() / connect()

$options = new ZOOM::Options();
$options->option(implementationName => "my client");
$options->option(implementationId => 12345);
$conn = create ZOOM::Connection($options)
# or
$conn = create ZOOM::Connection(implementationName => "my client",
                                implementationId => 12345);

$conn->connect($host, 0);

The usual Connection constructor, new(), brings a new object into existence and forges the connection to the server, all in one single operation โ€” which is often exactly what you want, no notes. But for applications that need a little more control, these two methods split the two steps apart, so you can slip extra steps in between, like setting options. ๐Ÿ’…

create() creates and returns a new Connection object that is not connected to any server. You may pass it an options block of type ZOOM::Options (see below), where options can be set before or after the Connection gets created. Alternatively and equivalently, create() can be handed a list of key-value option pairs directly. Then you forge the connection to the server with the connect() method, which takes hostname and port arguments just like the ones new() constructor takes.

error_x() / errcode() / errmsg() / addinfo() / diagset()

($errcode, $errmsg, $addinfo, $diagset) = $conn->error_x();
$errcode = $conn->errcode();
$errmsg = $conn->errmsg();
$addinfo = $conn->addinfo();
$diagset = $conn->diagset();

These give you the tea โ˜• on the last error to have occurred on a connection โ€” although typically you won't have used them, because the same info is available through the ZOOM::Exception that gets thrown when the error happens. The errcode(), errmsg(), addinfo() and diagset() methods each return one element of the diagnostic, and error_x() returns all four at once. efficient, we stan.

See the ZOOM::Exception for how to interpret those elements.

exception()

die $conn->exception();

exception()

$conn->exception();

Hands you back the exact same info as error_x() but as a ZOOM::Exception object instead โ€” so you can throw it or render it however you like, bestie ๐Ÿ’…. And if nothing went wrong on the connection? Then exception() just returns an undefined value. No drama, no error. โœจ


check()

$conn->check();

Looks at the connection and goes "is there an error pending rn?" โ€” and if there is, it throws a ZOOM::Exception object. ๐Ÿซถ

Okay so important: with synchronous connections, errors get thrown the moment they happen, so you literally never need to call this. It's only a thing for asynchronous applications. Periodt.

option() / option_binary()

print("server is '", $conn->option("serverImplementationName"), "'\n");
$conn->option(preferredRecordSyntax => "usmarc");
$conn->option_binary(iconBlob => "foo\0bar");
die if length($conn->option_binary("iconBlob") != 7);

So โ€” Connection, ResultSet, ScanSet and Package objects all walk around carrying a lil bag of named options that change how they act in certain situations. Full deets live in the ZOOM-C options docs: ๐Ÿ‘‡

Connection options are listed at http://www.indexdata.com/yaz/doc/zoom.html#zoom-connections

You set and fetch these with the option() method, and it's flexible like that โ€” call it with one argument or two:

  • two args: the option named by the first arg gets set to the value of the second arg, and you get the old value back.
  • one arg: you just get the current value of that option. Simple. Slay. โœจ

โš ๏ธ For historical reasons, option values are not binary-clean โ€” so a value containing a NUL byte comes back to you in truncated form. Rude tbh. That's exactly why option_binary() exists: it does the identical thing but it is binary-clean, so values with NUL bytes get set and returned correctly. ๐Ÿ’–


search() / search_pqf()

$rs = $conn->search(new ZOOM::Query::CQL('title=dinosaur'));
# The next two lines are equivalent
$rs = $conn->search(new ZOOM::Query::PQF('@attr 1=4 dinosaur'));
$rs = $conn->search_pqf('@attr 1=4 dinosaur');

A search-and-retrieve protocol's whole main character energy is searching (and, er, retrieving), so obviously the principal method on a Connection is search(). ๐Ÿ’…

It takes a single argument โ€” a ZOOM::Query object (or, being precise, an object of a subclass of that class) โ€” and it creates and returns a brand new ResultSet object representing the set of records that came out of the search.

And because PQF (Prefix Query Format) queries are so common, we give them the special-case treatment with search_pqf(). It's identical to search() except it takes a string containing the query instead of an object, so you don't have to build a ZOOM::Query::PQF object yourself. ๐ŸŽ€ PQF deets live in that class's docs.


scan() / scan_pqf()

$rs = $conn->scan(new ZOOM::Query::CQL('title=dinosaur'));
# The next two lines are equivalent
$rs = $conn->scan(new ZOOM::Query::PQF('@attr 1=4 dinosaur'));
$rs = $conn->scan_pqf('@attr 1=4 dinosaur');

Tons of Z39.50 servers let you browse their indexes to find terms to search for. That's the scan method's job โ€” it creates and returns a new ScanSet object representing the set of terms that came out of the scan. ๐Ÿฆ‹

scan() takes a single argument but omg it works so hard for that one argument: it has to say both what index to scan for terms AND where in that index to start scanning. And it gets worse โ€” specifying the index involves multiple facets, like which database fields it indexes (author, subject, title, etc.) and whether we're scanning whole fields or single words. Like. Picture the title The Empire Strikes Back'' versus the four words Back'', Empire'', Strikes'' and ``The'' โ€” interleaved with words from other titles in the same index. Chaos. ๐Ÿ˜ญ

All of that gets handled by passing a Query object representing a query of a single term as the scan() argument:

  • the attributes attached to that term say which index to use
  • the term itself says the point in the index to start scanning from

So for example, if the argument is the query @attr 1=4 fish, then:

  • @attr 1=4 This is the BIB-1 attribute with type 1 (meaning access-point, which specifies an index), and type 4 (which means ``title''). So the scan is in the title index.
  • fish Start the scan from the lexicographically earliest term that is equal to or falls after ``fish''.

The argument @attr 1=4 @attr 6=3 fish would behave similarly โ€” but BIB-1 attribute 6=3 means completeness=``complete field'', so the scan would be for complete titles rather than for words occurring in titles.

Yeah. This takes a bit of getting used to. ๐Ÿ™ˆ

The behaviour of scan() is affected by these options, which you can set on the Connection you're scanning through:

  • number [default: 10] โ€” how many terms should come back in the ScanSet. The number you actually get may be less if the start-point is near the end of the index โ€” but never greater.
  • position [default: 1] โ€” a 1-based index saying where in the returned list of terms the seed-term should appear. By default it's the first term returned, but you can set position to, say, zero (asking for the next terms after the seed-term), or to the same value as number (asking for the index terms before the seed term).
  • stepSize [default: 0] โ€” an integer saying how many indexed terms get skipped between each one returned in the ScanSet. By default nothing is skipped, but overriding this is lowkey useful for getting a high-level overview of the index.

And since scans using PQF (Prefix Query Format) are so common, we make them a special case with scan_pqf(). It's identical to scan() except it accepts a string containing the query rather than an object, so you don't have to create a ZOOM::Query::PQF object. ๐Ÿ‘‘


package()

$p = $conn->package();
$o = new ZOOM::Options();
$o->option(databaseName => "newdb");
$p = $conn->package($o);

Creates and returns a new ZOOM::Package, which you use for invoking an Extended Service. You can optionally pass in an options block. See the ZOOM::Package documentation for the full tea. ๐Ÿซ–


last_event()

if ($conn->last_event() == ZOOM::Event::CONNECT) {
    print "Connected!\n";
}

Returns a ZOOM::Event enumerated value telling you the type of the last event that happened on the connection. This one is only for complex asynchronous applications โ€” check the sections below about the ZOOM::Event enumeration and async apps. โœจ


destroy()

$conn->destroy()

Destroys a Connection object: tears down any low-level connection attached to it and frees up its resources. ๐Ÿงš

It is an error to reuse a Connection that has been destroy()ed. Like, do not do that. It's giving haunted. ๐Ÿ’€

ZOOM::ResultSet

$rs = $conn->search_pqf('@attr 1=4 mineral');
$n = $rs->size();
for $i (1 .. $n) {
    $rec = $rs->record($i-1);
    print $rec->render();
}

A ResultSet object represents the set of zero or more records resulting from a search, and it's how you actually get at those records. It may also keep a client-side cache of some, less, none, all โ€” or even more โ€” of the server's records. In general that's meant to be an implementation detail you never have to think about, though fancier applications do have facilities for messing with that cache. Most apps only need size(), record() and sort(). ๐Ÿ’…

There is no new() method, and no other explicit constructor either. The only way to create a ResultSet is by calling search() (or search_pqf()) on a Connection. That's it. That's the door. ๐Ÿšชโœจ

See the description of the ``Result Set'' class in the ZOOM Abstract API at http://zoom.z3950.org/api/zoom-current.html#3.4

Methods


option()

$rs->option(elementSetName => "f");

Lets you set options into โ€” and read options from โ€” a ResultSet, exactly like the Connection class's option() method. ๐ŸŽ€ But heads up: there is no option_binary() method for ResultSet objects.

ResultSet options are listed at http://www.indexdata.com/yaz/doc/zoom.resultsets.html


size()

print "Found ", $rs->size(), " records\n";

Returns the number of records in the result set. Simple, iconic, does what it says. โญ


record() / record_immediate()

$rec = $rs->record(0);
$rec2 = $rs->record_immediate(0);
$rec3 = $rs->record_immediate(1)
    or print "second record wasn't in cache\n";

record() returns a ZOOM::Record object representing a record from the result-set, with the position given by the argument you pass in. It's a zero-based index, so legit values run from zero to $rs->size()-1. ๐Ÿฆ‹

record_immediate() has an identical API, but it never kicks off a network operation โ€” it just grabs the record from the ResultSet's cache if it's already sitting there, and gives you an undefined value if it isn't.

So if you use this method, you must always check the return value. Always. Fr fr. ๐Ÿ™ˆ

records()

$rs->records(0, 10, 0);
for $i (0..10) {
    print $rs->record_immediate($i)->render();
}

@nextseven = $rs->records(10, 7, 1);

Okay so here's the whole situation. record_immediate() only fetches from the cache, whereas record() goes all the way to the server if the record isn't cached yet โ€” but the ZOOM module has to guess what the most efficient strategy is, and bestie, it can't read your mind. ๐Ÿ˜ญ

It might fetch each record alone when you ask for it โ€” which is optimal if you only care about the top hit from each search, but absolutely pessimal if you want to display a whole list of results. Or conversely, it might always ask for blocks of twenty records โ€” great for assembling long lists of things, wasteful when you only want one. The problem is that ZOOM can't tell, when you call $rs->record(), what your intention is. ๐Ÿ™ƒ

But you can tell it. ๐Ÿ’… The records() method fetches a sequence of records, all in one go. It takes three arguments:

  1. the zero-based index of the first record in the sequence
  2. the number of records to fetch
  3. a boolean indicating whether or not to return the retrieved records as well as adding them to the cache

(You can always just pass 1 for that third one if you want โ€” Perl will discard the unused return value โ€” but there's a small efficiency gain to be had by passing 0. ๐ŸŒธ)

Once the records have been retrieved from the server (i.e. records() has completed without throwing an exception), you can grab them way more efficiently using record() โ€” or record_immediate(), which is then guaranteed to succeed. โœจ


cache_reset()

$rs->cache_reset()

Resets the ResultSet's record cache, so that later invocations of record_immediate() will fail. Ngl I struggle to imagine a real scenario where you'd want to do this. ๐Ÿ’€


sort()

if ($rs->sort("yaz", "1=4 >i 1=21 >s") < 0) {
    die "sort failed";
}

Sorts the ResultSet in place โ€” and ngl, "in place" is doing a lot of work there, because it also yeets any cached records, since they're generally gonna end up in a totally different position after sorting. It takes two arguments: the first is a string saying what kind of sort-specification you're handing over, and the second is the specification itself. ๐Ÿ’…

The sort() method returns 0 on success, or -1 if your sort-specification is invalid. Periodt.

Right now, the only supported sort-specification type is "yaz". Here's the vibe: such a specification is a space-separated sequence of keys, and each key is itself two space-separated words โ€” so the total word count in the sort-specification is always even, bestie, do the math ๐Ÿงฎ. The two words making up each key are a field and a set of flags. The field comes in one of two forms:

  • If it contains an = sign, it's a BIB-1</> type=value pair specifying which field to sort โ€” like "1=4" for a title sort โœจ
  • Otherwise, it just gets sent off for the server to interpret as best it can ๐Ÿ™ˆ

The flags word is built from one or more of these:

  • "s" โ€” case sensitive
  • "i" โ€” case insensitive
  • "<" โ€” ascending order
  • ">" โ€” descending order

So like, the sort-specification in that code fragment up there? It sorts the records in $rs case-insensitively in descending order of title, with records that have equivalent titles then sorted case-sensitively in ascending order of subject ๐ŸŒธ. (For the record, the BIB-1 access points 4 and 21 represent title and subject respectively. iykyk ๐Ÿ‘‘)

destroy()

$rs->destroy()

Destroys a ResultSet object and frees its resources. ๐Ÿ’€ And bestie, listen โ€” it is an error to reuse a ResultSet that's already been destroy()ed. Don't do it. She's gone, let her rest.


๐Ÿฆ‹ ZOOM::Record ๐Ÿฆ‹

$rec = $rs->record($i);
print $rec->render();
$raw = $rec->raw();
$marc = new_from_usmarc MARC::Record($raw);
print "Record title is: ", $marc->title(), "\n";

A Record object represents a record that's been retrieved from the server. Like, a single record, out there living its life. ๐ŸŽ€

There is no new() method, and no other explicit constructor either. The only way to create a new Record is by using record() (or record_immediate(), or records()) on a ResultSet. That's the door, that's the only door ๐Ÿšชโœจ

In general, records are "owned" by the result-sets they were retrieved from, so you don't have to do any explicit memory-management โ€” they get deallocated (and therefore can no longer be used ๐Ÿ˜ญ) when the result-set is destroyed.

See the description of the "Record" class in the ZOOM Abstract API at http://zoom.z3950.org/api/zoom-current.html#3.5

Methods

error() / exception()

if ($rec->error()) {
    my($code, $msg, $addinfo, $dset) = $rec->error();
    print "error $code, $msg ($addinfo) from $dset set\n";
    die $rec->exception();
}

These functions test for surrogate diagnostics associated with a record โ€” so basically errors that pertain to one particular record rather than to the fetch-some-records operation as a whole. (That second kind are known in Z39.50 as <italaic>non-surrogate diagnostics</italaic>, and they get reported as exceptions thrown by searches.) So if a particular record can't be obtained โ€” say, because it's not available in the requested record syntax โ€” then the record object you got from the result-set, when you interrogate it with these functions, will report the error. ๐Ÿ’…

error() returns the error-code, a human-readable message, additional information, and the name of the diagnostic set that the error is from. When you call it in scalar context, it just returns the error-code. And since error 0 means "no error", you can straight up use it as a boolean has-there-been-an-error indicator ๐Ÿซถ

exception() returns the same information in the form of a "ZOOM::Exception" object, which may be thrown or rendered. If no error occurred on the record, then exception() returns an undefined value.

render()

print $rec->render();
print $rec->render("charset=latin1,utf8");

Returns a human-readable representation of the record ๐ŸŒธ. Beyond that? No promises, bestie. Careful programs should not make assumptions about the format of the returned string. It's giving "vibes only, no contract."

If the optional argument is provided, then it's interpreted as in the get() method (q.v.).

This method is useful mostly for debugging. ๐Ÿ”

raw()

use MARC::Record;
$raw = $rec->raw();
$marc = new_from_usmarc MARC::Record($raw);
$trans = $rec->render("charset=latin1,utf8");

Returns an opaque blob of data that is the raw form of the record. Exactly what that is, and what you can do with it, varies depending on the record-syntax. For example, XML records will be returned as, well, XML ๐Ÿ’€; MARC records will be returned as ISO 2709-encoded blocks that can be decoded by software such as the fine "Marc::Record" module; GRS-1 record will be โ€ฆ gosh, what an interesting question ๐Ÿงš. But no-one uses GRS-1 any more, do they?

If the optional argument is provided, then it's interpreted as in the get() method (q.v.).

get()

$raw = $rec->get("raw");
$rendered = $rec->get("render");
$trans = $rec->get("render;charset=latin1,utf8");
$trans = $rec->get("render", "charset=latin1,utf8");

This is the underlying method used by render() and raw(), and it in turn delegates to the ZOOM_record_get() function of the underlying ZOOM-C library. Most applications will find it way more natural to just work with render() and raw() โœจ

get() may be called with either one or two arguments. The two-argument form is syntactic sugar: the two arguments are simply joined with a semi-colon to make a single argument, so the third and fourth example invocations above are equivalent. The second argument (or the portion of the first argument following the semicolon) is used in the "type" argument of ZOOM_record_get(), as described in http://www.indexdata.com/yaz/doc/zoom.records.html. This is useful primarily for invoking the character-set transformation โ€” in the examples above, from ISO Latin-1 to UTF-8 Unicode ๐ŸŒˆ

clone() / destroy()

$rec = $rs->record($i);
$newrec = $rec->clone();
$rs->destroy();
print $newrec->render();
$newrec->destroy();

Usually it's super convenient that Record objects are owned by their ResultSets and just go away when the ResultSet is destroyed โ€” but occasionally you need a Record to outlive its parent and destroy it later, explicitly. To do that: clone() the record, keep the new Record object that gets returned, and destroy() it when it's no longer needed. And that right there is the only situation in which a Record needs to be destroyed. Iconic, honestly ๐ŸŽ€


๐Ÿ’œ ZOOM::Exception ๐Ÿ’œ

In general, method calls throw an exception (of class "ZOOM::Exception") if anything goes wrong, so you don't need to test for success after each and every call โ€” huge relief, right? ๐Ÿ˜ญ Exceptions are caught by enclosing the main code in an "eval{}" block and checking $@ on exit from that block, as in the code-sample above.

There are a small number of exceptions to this rule: the three record-fetching methods in the "ZOOM::ResultSet" class โ€” record(), record_immediate(), and records() โ€” can all return undefined values for legitimate reasons, under circumstances that do not merit throwing an exception. For this reason, the return values of these methods should be checked. See the individual methods' documentation for details ๐Ÿ‘‘

An exception carries the following pieces of information:

  • error-code โ€” A numeric code that specifies the type of error. This can be checked for equality with known values, so that intelligent applications can take appropriate action.
  • error-message โ€” A human-readable message corresponding with the code. This can be shown to users, but its value should not be tested, as it could vary in different versions or under different locales.
  • additional information [optional] โ€” A string containing information specific to the error-code. For example, when the error-code is the BIB-1 diagnostic 109 ("Database unavailable"), the additional information is the name of the database that the application tried to use. For some error-codes there is no additional information at all; for some others, the additional information is undefined, and may just be a human-readable string.
  • diagnostic set [optional] โ€” A short string specifying the diagnostic set from which the error-code was drawn: for example, "ZOOM" for a ZOOM-specific error such as "ZOOM::Error::MEMORY" ("out of memory"), and "BIB-1" for a Z39.50 error-code drawn from the BIB-1 diagnostic set.

In theory, the error-code should be interpreted in the context of the diagnostic set from which it's drawn; in practice, nearly all errors are from either the ZOOM or BIB-1 diagnostic sets, and the codes in those sets have been chosen so as not to overlap, so the diagnostic set can usually be ignored ๐ŸŒธ (slay, honestly)

See the description of the "Exception" class in the ZOOM Abstract API at http://zoom.z3950.org/api/zoom-current.html#3.7

Methods

new()

die new ZOOM::Exception($errcode, $errmsg, $addinfo, $diagset);

Creates and returns a new Exception object with the specified error-code, error-message, additional information and diagnostic set. Applications won't in general need to use this, but may find it useful to simulate ZOOM exceptions. And as is usual with Perl, exceptions are thrown using die(). ๐Ÿ’€

code() / message() / addinfo() / diagset()

print "Error ", $@->code(), ": ", $@->message(), "\n";
print "(addinfo '", $@->addinfo(), "', set '", $@->diagset(), "')\n";

These methods take no arguments, and return the exception's error-code, error-message, additional information and diagnostic set respectively. Easy ๐Ÿซถ

render()

print $@->render();

Returns a human-readable rendition of an exception. And here's the cute part โ€” the "" operator is overloaded on the Exception class, so an Exception used in a string context gets rendered automatically โœจ Among other consequences, this has the useful result that a ZOOM application that died due to an uncaught exception will emit an informative message before exiting. She goes out with dignity ๐Ÿ’…


๐ŸŒˆ ZOOM::ScanSet ๐ŸŒˆ

$ss = $conn->scan('@attr 1=1003 a');
$n = $ss->size();
($term, $occ) = $ss->term($n-1);
$rs = $conn->search_pqf('@attr 1=1003 "' . $term . '"');
assert($rs->size() == $occ);

A ScanSet represents a set of candidate search-terms returned from an index scan. Its sole purpose in life is to give you access to those terms, to the corresponding display terms, and to the occurrence-counts of the terms. That's it, that's the whole job ๐ŸŽ€

There is no new() method, nor any other explicit constructor. The only way to create a new ScanSet is by using scan() on a Connection.

See the description of the "Scan Set" class in the ZOOM Abstract API at http://zoom.z3950.org/api/zoom-current.html#3.6

Methods

size()

print "Found ", $ss->size(), " terms\n";

So this one hands you back the number of terms in the scan set, periodt. ๐Ÿ’… In general that's exactly the scan-set size you asked for with the "number" option on the Connection where you ran your scan โ€” and the default there is 10 โ€” but lowkey it might be fewer if your scan is sitting close to the end of the index. ๐ŸŒธ

term() / display_term()

$ss = $conn->scan('@attr 1=1004 whatever');
($term, $occurrences) = $ss->term(0);
($displayTerm, $occurrences2) = $ss->display_term(0);
assert($occurrences == $occurrences2);
if (user_likes_the_look_of($displayTerm)) {
    $rs = $conn->search_pqf('@attr 1=4 "' . $term . '"');
    assert($rs->size() == $occurrences);
}

These two besties give you back the actual scanned terms themselves. ๐Ÿ”ฅ term() gives you the term in a form that's suitable for submitting as part of a query, whereas display_term() gives it to you in a form that's suitable for showing to an actual human user. Both versions also return the number of occurrences of that term in the index โ€” meaning the number of hits you'll get if you go ahead and use that term in a query later. โœจ

In most cases the term and the display term are gonna be identical, ngl. But they can differ in cases where punctuation or case gets normalised, or where identifiers rather than the original document terms are what got indexed. ๐Ÿฆ‹

option()

print "scan status is ", $ss->option("scanStatus");

Lets you set options into a ScanSet, and read options back out of it, exactly like the Connection class's option() method. Just so you know: there is no option_binary() method for ScanSet objects. ๐Ÿ™ˆ

ScanSet options are also described โ€” though, and I'm quoting here, not particularly informatively โ€” at http://www.indexdata.com/yaz/doc/zoom.scan.html

destroy()

$ss->destroy()

Destroys a ScanSet object and frees its resources. It is an error to reuse a ScanSet that has been destroy()ed, so like... don't. ๐Ÿ’€


ZOOM::Package

$p = $conn->package();
$p->option(action => "specialUpdate");
$p->option(recordIdOpaque => 145);
$p->option(record => content_of("/tmp/record.xml"));
$p->send("update");
$p->destroy();

This class represents an Extended Services Package: basically an instruction to the server to do something that the core parts of the Z39.50 standard don't cover (or the SRW/SRU equivalent of that). ๐Ÿ‘‘ Since the core protocols are read-only, requests like this are often how you actually make changes to the database โ€” like in that record update example up there. ๐Ÿ’…

Requesting an extended service is a four-step process:

  1. First, create a package associated with the connection to the relevant database. ๐Ÿงš
  2. Second, set options on the package to tell the server what to do. ๐ŸŽ€
  3. Third, send the package โ€” and fair warning, this may result in an exception being thrown if the server can't execute the operations you asked for. ๐Ÿ˜ญ
  4. And finally, destroy the package. ๐ŸŒˆ

Package options are listed at http://www.indexdata.com/yaz/doc/zoom.extendedservices.html

Which options actually mean anything is determined by the top-level operation string you specify as the argument to send(). For example, when the operation is "update" (the most commonly used extended service, fr fr), the "action" option may be set to any of:

  • "recordInsert" โ€” add a new record, failing if that record already exists
  • "recordDelete" โ€” delete a record, failing if it is not in the database
  • "recordReplace" โ€” replace a record, failing if an old version is not already present
  • "specialUpdate" โ€” add a record, replacing any existing version that may be present

For update, the "record" option should be set to the full text of the XML record to be added, deleted or replaced. Depending on how the server is configured, it may extract the record's unique ID from the text itself (i.e. from a known element such as the 001 field of a MARCXML record), or it may require the unique ID to be passed in explicitly using the "recordIdOpaque" option. ๐Ÿ“

Extended services packages are not currently described in the ZOOM Abstract API at http://zoom.z3950.org/api/zoom-current.html โ€” they'll be added in a forthcoming version, and will function much as those implemented in this module. โญ

Methods

option()

$p->option(recordIdOpaque => "46696f6e61");

Lets you set options into a Package, and read them out of it, just like the Connection class's option() method. And yep, same deal as before: there is no option_binary() method for Package objects. ๐Ÿ™…

Package options are listed at http://www.indexdata.com/yaz/doc/zoom.extendedservices.html

send()

$p->send("create");

Sends a package to the server associated with the Connection that created it. Problems are reported by throwing an exception. ๐Ÿ‘€ The single parameter indicates the operation the server is being asked to perform, and it controls how the package's options get interpreted. Valid operations include:

  • itemorder โ€” request a copy of a nominated object, e.g. place an ILL request
  • create โ€” create a new database, whose name is specified by the "databaseName" option
  • drop โ€” drop an existing database, whose name is specified by the "databaseName" option
  • commit โ€” commit changes made to the database within a transaction
  • update โ€” modify the contents of the database by adding, deleting or replacing records (as described up in the overview of the "ZOOM::Package" class)
  • xmlupdate โ€” I have no idea what this does. ๐Ÿ’€

Even though this module can make all these requests, not all servers can execute them. Refusal shows up as a thrown exception. Problems can also come from a lack of privileges โ€” so send() must be used with caution, and it's perhaps best wrapped in a clause that checks for exceptions, like so:

eval { $p->send("create") };
if ($@ && $@->isa("ZOOM::Exception")) {
    print "Oops!  ", $@->message(), "\n";
    return $@->code();
}

destroy()

$p->destroy()

Destroys a Package object and frees its resources. It is an error to reuse a Package that has been destroy()ed. ๐Ÿšซ


ZOOM::Query

$q = new ZOOM::Query::CQL("creator=pike and subject=unix");
$q->sortby("1=4 >i 1=21 >s");
$rs = $conn->search($q);
$q->destroy();

"ZOOM::Query" is a virtual base class, and various concrete subclasses can be derived from it. Different subclasses implement different types of query. The sole purpose of a Query object is to be used in a search() on a Connection โ€” and because PQF is such a common special case, the shortcut Connection method search_pqf() exists for exactly that. ๐Ÿซถ

The following Query subclasses are provided, and each one gives you the same set of methods described down below:

  • ZOOM::Query::PQF โ€” Implements Prefix Query Format (PQF), sometimes also known as Prefix Query Notation (PQN). This esoteric but rigorous and expressive format is described in the YAZ Manual at http://www.indexdata.com/yaz/doc/tools.html#PQF
  • ZOOM::Query::CQL โ€” Implements the Common Query Language (CQL) of SRU, the Search/Retrieve URL. CQL is a much friendlier notation than PQF, using a simple infix notation. โœจ The queries are passed "as is" to the server rather than being compiled into a Z39.50 Type-1 query, so only CQL-compliant servers can support queries like this. CQL is described at http://www.loc.gov/standards/sru/cql/ and in a slightly out-of-date but nevertheless useful tutorial at http://zing.z3950.org/cql/intro.html
  • ZOOM::Query::CQL2RPN โ€” Implements CQL by compiling it on the client-side into a Z39.50 Type-1 (RPN) query, and sending that instead. This gives you essentially the same functionality as "ZOOM::Query::CQL", but it'll work against any standard Z39.50 server rather than only against the small subset that support CQL natively. ๐Ÿ‘‘ The drawback? Because the compilation happens client-side, you need a configuration file to direct the mapping of CQL constructs โ€” index names, relations and modifiers โ€” into Type-1 query attributes. An example CQL configuration file is included in the ZOOM-Perl distribution, in the file "samples/cql/pqf.properties"
  • ZOOM::Query::CCL2RPN โ€” Implements CCL by compiling it on the client-side into a Z39.50 Type-1 (RPN) query, and sending that. Because the compilation is done on the client side, a configuration file is required to direct the mapping of CCL constructs such as index names and boolean operators into Type-1 query attributes. An example CCL configuration file is included in the ZOOM-Perl distribution, in the file "samples/ccl/default.bib"

CCL is syntactically very similar to CQL, but way looser. While CQL is an entirely precise language in which every possible query has rigorously defined semantics โ€” which is exactly why it's suitable for transfer as part of a protocol โ€” CCL is best deployed as a human-facing UI language. ๐Ÿ’…

See the description of the "Query" class in the ZOOM Abstract API at http://zoom.z3950.org/api/zoom-current.html#3.3

Methods

new()

$q = new ZOOM::Query::CQL('title=dinosaur');
$q = new ZOOM::Query::PQF('@attr 1=4 dinosaur');

Creates a new query object, compiling the query you passed as its argument according to the rules of whichever particular query-type you're instantiating. If the compilation fails, an exception gets thrown. Otherwise, the query may be passed to the "Connection" method search(). ๐Ÿ”ฅ

$conn->option(cqlfile => "samples/cql/pqf.properties");
$q = new ZOOM::Query::CQL2RPN('title=dinosaur', $conn);

Heads up: for the "ZOOM::Query::CQL2RPN" subclass, the Connection must also be passed into the constructor. It gets used for two things โ€” first, its "cqlfile" option is used to find the CQL configuration file that directs the translations into RPN; and second, if the compilation fails, diagnostic information gets cached in the Connection and can be retrieved using $conn->errcode() and related methods. ๐Ÿฆ‹

$conn->option(cclfile => "samples/ccl/default.bib");
# or
$conn->option(cclqual => "ti u=4 s=pw\nab u=62 s=pw");
$q = new ZOOM::Query::CCL2RPN('ti=dinosaur', $conn);

Same energy for the "ZOOM::Query::CCL2RPN" subclass โ€” the Connection has to be passed into the constructor too, for the exact same reasons as when client-side CQL compilation is used. The "cclqual" option, if it's defined, gives a CCL qualification specification inline; otherwise, the contents of the file named by the "cclfile" option are used instead. ๐ŸŽ€

sortby()

$q->sortby("1=4 >i 1=21 >s");

Sets a sort specification into the query, so that when a search() runs on that query, the result comes back automatically sorted. ๐ŸŒˆ The sort specification language is the same as the "yaz" sort-specification type of the "ResultSet" method sort(), described above.

destroy()

$p->destroy()

Destroys a Query object and frees its resources. It is an error to reuse a Query that has been destroy()ed. ๐Ÿ’€

โœจ ZOOM::Options โœจ

$o1 = new ZOOM::Options();
$o1->option(user => "alf");
$o2 = new ZOOM::Options();
$o2->option(password => "fruit");
$opts = new ZOOM::Options($o1, $o2);
$conn = create ZOOM::Connection($opts);
$conn->connect($host); # Uses the specified username and password

Okay so bestie, several classes of ZOOM objects carry their own sets of options, and you can manipulate them with their option() method. But sometimes it's literally useful to deal with the option sets directly, and that's exactly why the ZOOM::Options class exists. ๐Ÿ’…

Option sets are not currently described in the ZOOM Abstract API at http://zoom.z3950.org/api/zoom-current.html They are an extension to that specification. So don't @ me, it's an extension, periodt. ๐Ÿฆ‹

โœจ Methods โœจ

  • new() โ€” creates and returns a new option set. One or two (but no more) existing option sets may be passed as arguments, in which case they become "parents" of the new set, which thereby "inherits" their options, the values of the first parent overriding those of the second when both have a value for the same key. An option set that inherits from a parent that has its own parents also inherits the grandparent's options, and so on. iykyk ๐Ÿ’–
$o1 = new ZOOM::Options();
$o1and2 = new ZOOM::Options($o1);
$o3 = new ZOOM::Options();
$o1and3and4 = new ZOOM::Options($o1, $o3);
  • option() / option_binary() โ€” these methods are used to get and set options within a set, and behave the same way as the same-named Connection methods - see above. As with the Connection methods, values passed to and retrieved using option() are interpreted as NUL-terminated, while those passed to and retrieved from option_binary() are binary-clean. โœจ
$o->option(preferredRecordSyntax => "usmarc");
$o->option_binary(iconBlob => "foo\0bar");
die if length($o->option_binary("iconBlob") != 7);
  • bool() โ€” the first argument is a key, and the second is a default value. Returns the value associated with the specified key as a boolean, or the default value if the key has not been set. The values "T" (upper case) and 1 are considered true; all other values (including "t" (lower case) and non-zero integers other than one) are considered false. ๐Ÿ“

This method is provided in ZOOM-C because in a statically typed language it's convenient to have the result returned as an easy-to-test type. In a dynamically typed language such as Perl, this problem doesn't arise, so bool() is nearly useless; but it is made available in case applications need to duplicate the idiosyncratic interpretation of truth and falsehood and ZOOM-C uses. Not me explaining ZOOM-C lore ๐Ÿ’€

$o->option(x => "T");
$o->option(y => "F");
assert($o->bool("x", 1));
assert(!$o->bool("y", 1));
assert($o->bool("z", 1));
  • int() โ€” returns the value associated with the specified key as an integer, or the default value if the key has not been set. See the description of bool() for why you almost certainly don't want to use this. fr fr
$o->option(x => "012");
assert($o->int("x", 20) == 12);
assert($o->int("y", 20) == 20);
  • set_int() โ€” sets the value of the specified option as an integer. Of course, Perl happily converts strings to integers on its own, so you can just use option() for this, but set_int() is guaranteed to use the same string-to-integer conversion as ZOOM-C does, which might occasionally be useful. Though I can't imagine how. I'm just the messenger ๐Ÿ’…
$o->set_int(x => "29");
  • set_callback() โ€” this method allows a callback function to be installed in an option set, so that the values of options can be calculated algorithmically rather than, as usual, looked up in a table. Along with the callback function itself, an additional datum is provided: when an option is subsequently looked up, this datum is passed to the callback function along with the key; and its return value is returned to the caller as the value of the option. main character energy โœจ
sub cb {
    ($udata, $key) = @;
    return "$udata-$key-$udata";
}
$o->set_callback(\&cb, "xyz");
assert($o->option("foo") eq "xyz-foo-xyz");
Warning. Although it ought to be possible to specify callback function using the "\&name" syntax above, or a literal "sub { code }" code reference, the complexities of the Perl-internal memory management system mean that the function must currently be specified as a string containing the fully-qualified name, e.g. "main::cb".
Warning. The current implementation of this method leaks memory, not only when the callback is installed, but on every occasion that it is consulted to look up an option value. No cap, this is a real bug bestie ๐Ÿ˜ญ
  • destroy() โ€” destroys an Options object, freeing its resources. It is an error to reuse an Options object that has been destroy()ed. So don't be messy ๐Ÿ’œ
$o->destroy()

โœจ ENUMERATIONS โœจ

The ZOOM module provides two enumerations that list possible return values from particular functions. They are described in the following sections. Slay, let's get into it ๐Ÿฆ‹

โœจ ZOOM::Error โœจ

if ($@->code() == ZOOM::Error::QUERY_PQF) {
    return "your query was not accepted";
}

This class provides a set of manifest constants representing some of the possible error codes that can be raised by the ZOOM module. The methods that return error-codes are ZOOM::Exception::code(), ZOOM::Connection::error_x() and ZOOM::Connection::errcode(). Noted ๐Ÿ“

The ZOOM::Error class provides the constants NONE, CONNECT, MEMORY, ENCODE, DECODE, CONNECTION_LOST, ZINIT, INTERNAL, TIMEOUT, UNSUPPORTED_PROTOCOL, UNSUPPORTED_QUERY, INVALID_QUERY, CQL_PARSE, CQL_TRANSFORM, CCL_CONFIG, CCL_PARSE, CREATE_QUERY, QUERY_CQL, QUERY_PQF, SORTBY, CLONE, PACKAGE, SCANTERM and LOGLEVEL, each of which specifies a client-side error. These codes constitute the ZOOM diagnostic set. Iconic list tbh ๐Ÿ‘‘

Since errors may also be diagnosed by the server, and returned to the client, error codes may also take values from the BIB-1 diagnostic set of Z39.50, listed at the Z39.50 Maintenance Agency's web-site at http://www.loc.gov/z3950/agency/defns/bib1diag.html

All error-codes, whether client-side from the ZOOM::Error enumeration or server-side from the BIB-1 diagnostic set, can be translated into human-readable messages by passing them to the ZOOM::diag_str() utility function. So you can actually understand what went wrong โœจ

โœจ ZOOM::Event โœจ

if ($conn->last_event() == ZOOM::Event::CONNECT) {
    print "Connected!\n";
}

In applications that need it - mostly complex multiplexing applications - The ZOOM::Connection::last_event() method is used to return an indication of the last event that occurred on a particular connection. It always returns a value drawn from this enumeration, that is, one of NONE, CONNECT, SEND_DATA, RECV_DATA, TIMEOUT, UNKNOWN, SEND_APDU, RECV_APDU, RECV_RECORD, RECV_SEARCH or ZEND. Vibe check passed ๐ŸŒˆ

See the section below on asynchronous applications. Stay tuned bestie ๐Ÿ’–

โœจ LOGGING โœจ

ZOOM::Log::init_level(ZOOM::Log::mask_str("zoom,myapp,-warn"));
ZOOM::Log::log("myapp", "starting up with pid ", $$);

Logging facilities are provided by a set of functions in the ZOOM::Log module. Note that ZOOM::Log is not a class, and it is not possible to create ZOOM::Log objects: the API is imperative, reflecting that of the underlying YAZ logging facilities. Although there are nine logging functions altogether, you can ignore nearly all of them: most applications that use logging will begin by calling mask_str() and init_level() once each, as above, and will then repeatedly call log(). So much admin, but okay ๐Ÿ’…

โœจ mask_str() โœจ

$level = ZOOM::Log::mask_str("zoom,myapp,-warn");

Returns an integer corresponding to the log-level specified by the parameter. This is a string of zero or more comma-separated module-names, each indicating an individual module to be either added to the default log-level or removed from it (for those components prefixed by a minus-sign). The names may be those of either standard YAZ-logging modules such as fatal, debug and warn, or custom modules such as myapp in the example above. The module zoom requests logging from the ZOOM module itself, which may be helpful for debugging. Debugging era ๐Ÿ›

Note that calling this function does not in any way change the logging state: it merely returns a value. To change the state, this value must be passed to init_level(). Important! โญ

โœจ module_level() โœจ

$level = ZOOM::Log::module_level("zoom");
ZOOM::Log::log($level, "all systems clear: thrusters invogriated");

Returns the integer corresponding to the single log-level specified as the parameter, or zero if that level has not been registered by a prior call to mask_str(). Since log() accepts either a numeric log-level or a string, there is no reason to call this function; but, what the heck, maybe you enjoy that kind of thing. Who are we to judge? We love a curious queen ๐Ÿ‘‘

โœจ init_level() โœจ

ZOOM::Log::init_level($level);

Initialises the log-level to the specified integer, which is a bitmask of values, typically as returned from mask_str(). All subsequent calls to log() made with a log-level that matches one of the bits in this mask will result in a log-message being emitted. All logging can be turned off by calling init_level(0). Silence is golden ๐Ÿคซ

โœจ init_prefix() โœจ

ZOOM::Log::init_prefix($0);

Initialises a prefix string to be included in all log-messages. Branding, but for logs โœจ

โœจ init_file() โœจ

ZOOM::Log::init_file("/tmp/myapp.log");

Initialises the output file to be used for logging: subsequent log-messages are written to the nominated file. If this function is not called, log-messages are written to the standard error stream. Default: stderr, no cap ๐Ÿ’…

โœจ init() โœจ

ZOOM::Log::init($level, $0, "/tmp/myapp.log");

Initialises the log-level, the logging prefix and the logging output file in a single operation. One-stop shop ๐Ÿ›๏ธ

โœจ time_format() โœจ

ZOOM::Log::time_format("%Y-%m-%d %H:%M:%S");

Sets the format in which log-messages' timestamps are emitted, by means of a format-string like that used in the C function strftime(). The example above emits year, month, day, hours, minutes and seconds in big-endian order, such that timestamps can be sorted lexicographically. Sortable timestamps? Obsessed ๐Ÿฆ‹

โœจ init_max_size() โœจ

(This doesn't seem to work, so I won't bother describing it.) It's giving broken ๐Ÿ’€

โœจ log() โœจ

ZOOM::Log::log(8192, "reducing to warp-factor $wf");
ZOOM::Log::log("myapp", "starting up with pid ", $$);

Provided that the first argument, log-level, is among the modules previously established by init_level(), this function emits a log-message made up of a timestamp, the prefix supplied to init_prefix(), if any, and the concatenation of all arguments after the first. The message is written to the standard output stream, or to the file previously specified by init_file() if this has been called. So it's like a lil diary entry ๐Ÿ““

The log-level argument may be either a numeric value, as returned from module_level(), or a string containing the module name. Flexible queen ๐Ÿ’œ

๐Ÿฆ‹ ASYNCHRONOUS APPLICATIONS ๐Ÿฆ‹

okay bestie, async apps sound terrifying โ€” like genuinely conceptually a lot โ€” but the ZOOM girlies really said "we got you" and made it embarrassingly simple. The whole thing is literally four things:

  • one option: async
  • one function: ZOOM::event()
  • one Connection method: last_event()
  • one enumeration: ZOOM::Event

that's it. that's the whole vibe. ๐Ÿ’…

Here's how the process goes, step by step:

โœจ Initialisation โœจ

Make a bunch of connections to the various servers, and give each one the option async turned on, plus whatever other options you need โ€” like, say, the piggyback retrieval record-count, so records actually come back inside your search responses. multitasking queen behavior. ๐Ÿ‘‘

๐Ÿ’– Operations ๐Ÿ’–

Send searches to the connections, request records, etc. etc. โ€” you know the drill.

๐ŸŽ€ Event harvesting ๐ŸŽ€

Call ZOOM::event() over and over and over to find out what the servers are sending back. Every time it returns, it's telling you which connection has fired ๐Ÿ”ฅ โ€” so you go interrogate that connection with the last_event() method to see what actually happened, and then you test the return value (it's an element of the ZOOM::Event enumeration, periodt) to figure out your next move.

Case in point: the ZEND event means no more operations are outstanding on that connection, so any records you fetched are right there, ready to grab immediately. iconic.


Here's a genuinely tiny program (all error-checking omitted, bestie, don't @ me ๐Ÿ˜ญ) that shows the whole thing off. It parallel-searches three servers โ€” or more, if you add them to the list โ€” and shows the first record from each server's result-set the second it becomes available:

use ZOOM;
@servers = ('lx2.loc.gov:210/LCDB_MARC8',
            'z3950.indexdata.com:210/gils',
            'agricola.nal.usda.gov:7190/Voyager');
for ($i = 0; $i < @servers; $i++) {
    $z[$i] = new ZOOM::Connection($servers[$i], 0,
                                  async => 1, # asynchronous mode
                                  count => 1, # piggyback retrieval count
                                  preferredRecordSyntax => "usmarc");
    $r[$i] = $z[$i]->search_pqf("mineral");
}
while (($i = ZOOM::event(\@z)) != 0) {
    $ev = $z[$i-1]->last_event();
    print("connection ", $i-1, ": ", ZOOM::event_str($ev), "\n");
    if ($ev == ZOOM::Event::ZEND) {
        $size = $r[$i-1]->size();
        print "connection ", $i-1, ": $size hits\n";
        print $r[$i-1]->record(0)->render()
            if $size > 0;
    }
}

slay. ๐Ÿซถ


๐ŸŒˆ SEE ALSO ๐ŸŒˆ


๐Ÿ‘‘ AUTHOR ๐Ÿ‘‘

Mike Taylor, <mike@indexdata.com>

Copyright (C) 2005-2017 by Index Data.

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.8.4 or, at your option, any later version of Perl 5 you may have available.


and that's the whole page, bestie!! ๐ŸŽ‰ you made it all the way to the end, main character energy fr. this one's perl v5.42.3, dated 2026-08-19, filed under ZOOM(3pm) โ€” go forth and parallel-search, ily ๐Ÿ’–โœจ๐Ÿฆ‹

๐Ÿ“ฆ where this page comes from