Category: Uncategorized

Solr and boolean operators

December 1, 2011 at 12:13 pmCategory:Uncategorized

[Summary: ALWAYS ALWAYS ALWAYS USE PARENTHESES TO GROUP BOOLEANS IN SOLR!!!]

What does Solr do, given the following query?

  a OR b AND c

I’ll give you three guesses, but you’ll get the first two wrong and won’t have any idea how to generate a third, so don’t spend too much time on it.

Boolean algebra and operator precedence

Anyone who’s had even a passing introduction to boolean alegebra knows that it specifies a strict order to how the operators are bound: NOT before AND before OR. So, one might expect the following grouping:

  a OR (b AND c)

That’s guess one. It’s not how Solr does it.

Left to right?

Some naive students, and at least one programming language (Smalltalk), do a simple left-to-right evaluation. So you might go with:

  (a OR b) AND c

Nope. Wrong again.

So what’s left???

Excellent question. I don’t know the code well enough to know what’s going on underneath, but here’s what we get under the lucene query parser.

    (b AND c)

That’s right. The first term is thrown away.(More correctly, the first term is deemed “optional”).

Do you let your users put AND/OR/NOT in their queries?

Hopefully, they don’t know any boolean algebra. If they do, hopefully they use parentheses, or you parse it out for them. And if not, well, they’re gonna be pretty damn confused.

It gets weirder

I populated a fresh solr (3.5) index with all possible subsets of the strings “curly”, “larry”, “moe”, and “shemp” (not Joe. Don’t talk to me about Joe). There are 15 of them, from the one-item ‘curly’ to all four at once.

I wrote a script to run a set of queries against the index under both lucene and edismax to see what I would get. In all cases the default lucene operator is ‘AND’ and the edismax mm parameter is set to 100% (equivalent to “all required”).

        Lucene                    EDismax
--------------------------------------------------------

  1. curly AND larry curly larry curly larry
    curly larry moe curly larry moe
    curly larry shemp curly larry shemp
    curly larry moe shemp curly larry moe shemp

  2. curly AND larry OR moe curly curly larry
    curly larry curly larry moe
    curly moe curly larry shemp
    curly shemp curly larry moe shemp
    curly larry moe
    curly larry shemp
    curly moe shemp
    curly larry moe shemp

  3. curly OR larry AND moe larry moe larry moe
    curly larry moe curly larry moe
    larry moe shemp larry moe shemp
    curly larry moe shemp curly larry moe shemp

  4. curly AND larry OR moe AND shemp curly moe shemp curly larry moe shemp
    curly larry moe shemp

  5. moe AND shemp OR curly AND larry curly larry moe curly larry moe shemp
    curly larry moe shemp

Query 1 is as expected. Query 2 apparently reduces to just ‘curly’ under the lucene parser and ‘curly AND larry’ under edismax (and query 3 similarly reduces to the two AND’d words). Queries 4 and 5 are…well, you can look at the debugQuery output to see what it gets, but not why. And then tell me how to explain it to a user.

Where does this leave us?

The good news is that both lucene and edismax behave predictably when you use parentheses for grouping. So do that.

I’m generally not one to complain about open-source software, at least partially because I don’t have the chops to do anything about it most of the time, but I don’t understand how this could seem OK to anyone. There are a couple lucene Jira tickets (Lucene-167 and Lucene-1823) and a 2005 mailing list thread denouncing the current behavior, but it persists.

Until the Solr/Lucene powers that be decide to tackle this, the rest of us will either have to write pre-parsers to make sure users get something sensible, or cripple our applications to disallow unrestricted boolean queries.

2 Responses to “Solr and boolean operators”

  1. So the way I handle part of this at present is not actually passing these user-entered boolean queries straight to any solr query parser (not lucene, not edismax either; curious if edismax has the same or similar idiosyncracies, I predict it will).

    Instead, I actually parse all user queries in my own application, and then construct Solr queries (using a ‘lucene’ type query, with nested dismax type queries) that I know do what is needed for the user query to be interpreted with more typical boolean logic.

    Of course, it’s possible I’ve gotten it wrong too, but theoretically my parser/interpreter can then be fixed to what I want.

    Now, it might make even MORE sense to do this as an actual custom Solr query parser, but I lack the Solr/java comfort/skills to do that, so I’d rather do it in ruby. I think it would probably in some ways be better to do this in Java as a Solr plug-in query parser, rather than at the application layer, but oh well.

    Here’s the specs from my code that show how various user-entered queries are translated to Solr. Note they also take care of various types of “pure negative” queries that lucene and dismax query parsers can’t handle ‘right’ either (I think edismax fixes some but not all of these ‘pure negative’ cases). Hopefully the spec file is somewhat readable, despite (or because of!) the helper functions I put in to test generated solr query params against templates.

    https://github.com/projectblacklight/blacklight_advanced_search/blob/master/spec/parsing_nesting/to_solr_spec.rb

  2. PS: Sorry, I see you covered edismax too, thanks!

    PPS: One answer I’ve gotten from solr-ites is basically “Well, yeah, AND/OR aren’t really boolean operators to these query parsers, they are just indication of lucene optional/required clauses.”

    To which my answer is: “Okay, but then why use syntax that looks like boolean algebra, and that makes it very hard to predict exactly how they are translated to optional/required clauses, to the novice. Why not use a different syntax that actually indicates what it’s doing?” Perhaps the answer is “Well, becuase users are used to AND/OR”, but I think it’s no service to give users a syntax they are used to but with semantics that they are NOT used to!

    However, it’s possible that if you keep in mind “translating to lucene optional/mandatory clauses”, it will be the right mental model to figure out why the query parsers are doing what they’re doing. Although I still can’t figure it out, especially for some of the especially weird cases where terms are dropped altogether.

A short personal note

October 11, 2011 at 10:08 amCategory:Uncategorized

We had another baby. :-)

Shai Brown Dueber

Shai Brown Dueber was born last Monday, the 3rd, at a very moderate 7lbs 7.2oz (his brothers were 9lbs and 9.5lbs). Mother, baby, and older brothers are all doing well. Father is freakin’ tired.

Ziv, Nadav, and Shai

 

 

Comments are closed.

[Yes, another post about ruby code; I'll get back to library stuff soon.]

Quite a while ago, I released a little gem called threach (for “threaded #each”). It allows you to easily process a block with multiple threads.

  1.   # Process a CSV file with three threads
  2.   FIle.open('data.csv').threach(3, :each_line) {|line| send_to_db(line)}

Nice, right?

The problem is that I could never figure out a way to deal with a break or an Exception raised inside the block. The core problem is that once a thread trying to push/pop from a ruby SizedQueue is blocking, there’s no way (I could find) to tell it to wake up and see if there’s an error from another thread floating around that needs to be addressed.

So, I got into a pattern of running my code with each for a while, debugging, and eventually doing the production run under threach. Which is just dumb. Then I’d try to re-write threach to deal with this stuff using different approach (mutexes, lightweight events), quickly (or not so quickly) fail, give up, and start again.

So…let’s not worry MRI for the moment. I run all my big jobs under JRuby these days anyway, and there I can take advantage of Java’s blocking queues that have timeouts. When a queue operation times out, I can check to see if there’s been a break or an exception thrown in the meantime and behave appropriately.

The result is the gem jruby_threach. It works just like threach, except that, you know, it actually works the way I’d like it to.

  1. require 'jruby_threach'
  2. FIle.open('data.csv').threach(3, :each_line) {|line| send_to_db(line)}

Looks familiar, doesn’t it.

But you can also break out of the loop.

  1. myarray.threach(2) do |item|
  2.   break if item_indicates_to_break(item)
  3.   if item == :really_bad_value
  4.     raise RuntimeError.new, "Something's really wrong", nil
  5.   end
  6.   process_item(item)
  7. end

Any exceptions that are rescued within the block are handled internally and don’t cause processing to stop. Any that are not handled within the block are noticed by threach, cause the processing to stop, and the re-raised so you can deal with them outside of threach

  1.  
  2. reader = SpecializedFileReader.new(filename)
  3.  
  4. begin
  5.   reader.threach(2) do |item|
  6.     process_item(item)
  7.   end
  8. rescue SpecializedFileReaderError
  9.   # deal with the fact that the reader failed
  10. rescue Exception
  11.   # deal with the problem processing the item
  12. end

Dealing with the underlying Java data structures makes life a lot easier. To the point that I added an enhancement — threading production as well.

  1.   # Use two threads to read lines from files, and another three threads
  2.   # to process the data that comes out of those files.
  3.   Dir.glob("*.csv").map{|f| File.open(f)}.mthreach(2,3) do |item|
  4.     send_item_to_datbase(item)
  5.   end

mthreach basically allows you to treat an array of Enumerables as a single logical entity, multithreading both the producer and consumer sides of the operation. There aren’t a whole lot of obvious use cases, but it can certainly come in handy.

You can also access the underlying class that aggregates multiple enumerables directly.

  1. require 'jruby_threach'
  2. me = Threach::MultiEnum.new(
  3.   [enum1, enum2, enum3], # enumerables
  4.   threads,               # How many threads to use to
  5.   :each_with_index,      # the iterator to call on the enumerables
  6.   size                   # size of the under-the-hood queue
  7. )
  8.  
  9. # Note that like threach, calling #each against an MultiEnum actually
  10. # calls the iterator you sent in (in this case, #each_with_index)
  11. me.each {|item| process_item(item)}

3 Responses to “Even better, even simpler multithreading with JRuby”

  1. Bob Loblaw says:

    s/jquery_threach/jruby_threach/

  2. Bill says:

    Whoops! Thanks, and fixed.

  3. [...] written a jruby_specific threach that takes advantage of better underlying java libraries called jruby_threach that is a much better option if you're running [...]

I spent way too long asking my friend, The Internet, how to get a normal DBI connection to SQLIte3 using JRuby. Apparently, everyone except me is using ActiveRecord and/or Rails and doesn’t want to just connect to the database.

But I do. Here’s how.

First, get the gems:

  1.   gem install dbi
  2.   gem install dbd-jdbc
  3.   gem install jdbc-sqlite3

Then you’re ready to load it up into DBI.

  1. require 'rubygems' # if you're using 1.8 still
  2. require 'java'
  3. require 'dbi'
  4. require 'dbd/jdbc'
  5. require 'jdbc/sqlite3'
  6.  
  7. databasefile = 'test.db'
  8. dbh = DBI.connect(
  9.   "DBI:jdbc:sqlite:#{databasefile}",  # connection string
  10.   '',                                 # no username for sqlite3
  11.   '',                                 # no password for sqlite3
  12.   'driver' => 'org.sqlite.JDBC')      # need to set the driver
  13.  
  14. # That's it. Everything below here is stock DBI
  15.  
  16. dbh.do "create table squares (i integer, isquared integer)"
  17.  
  18. ins = dbh.prepare("insert into squares values (?, ?)")
  19. (1..20).each do |i|
  20.   ins.execute(i, i*i)
  21. end

Comments are closed.

How good is our relevancy ranking?

May 25, 2011 at 2:53 pmCategory:Uncategorized

For those of us that spend our days trying to tweak Mirlyn to make it better, one of the most important — and, in many ways, most opaque — questions is, “How good is our relevancy ranking?”

Research from the UMich Library’s Usability Group (pdf; 600k) points to the importance of relevancy ranking  for both known-item searches and discovery, but mapping search terms to the “best” results involves crawling deep inside the searcher’s head to know what she’s looking for.

So, what can we do?

Record interaction as a way of showing interest

One possibility is to look at those records that are somehow “touched” by a user in such a way that we can log it. If a user bothers to interact with an individual record, we’ll assume the record is interesting to her in the context of the current search.

There are three links associated with an individual record that a user can click on from the search results:

  • (62% of all record interactions) The title
  • (28%) An external link (HathiTrust, Google Books, or one of our vendors)
  • (10%) The “see holdings” link for those items that have multiple holdings

Our first issue arises quickly: only about a quarter of Mirlyn sessions contain any of these actions. For a full 75% of sessions, we have no data about which records users are paying attention to. They get a call number — or determine they have a failed search — and move on.

Where on the page do users interact with items?

We don’t know how users that interact with items differ from those that don’t. But for those that do, more than half of all record interactions are with the first record.

Here are the numbers for the first five records:

  • First record: 54%
  • Second record: 12%
  • Third record: 6%
  • Fouth record: 3.7%
  • Fifth record: 2.5%

More than 75% of all record interactions are with the first four items on the first page of results.

What does it all mean?

Frustratingly, we don’t know. Several possibilities are obvious:

  • we’re doing a good job with relevancy ranking
  • people do mostly known-item searches
  • people don’t bother looking past the first few results
  • excellent general search engines (e.g., Google) have trained people to believe that the first result is always worth a closer look.

The interactions between these (and unknown other) factors are likely complex.

In the meantime, though, to the extent these data can be extended to the general case (not at all obvious), we’re not doing too bad of a job.

Comments are closed.

I just released another (this time pretty good) version of my gem for normalizing/validating library standard numbers, library_stdnums (github source / docs).

The short version of the functions available:

  • ISBN: get checkdigit, validate, convert isbn10 to/from isbn13, normalize (to 13-digit)
  • ISSN: get checkdigit, validate, normalize
  • LCCN: validate, normalize

Validation of LCCNs doesn’t involve a checkdigit; I basically just normalize whatever is sent in and then see if the result is syntactically valid.

My plan in my Copious Free Time is to do a Java version of these as well and then stick them into a new-style Solr v.3 filter so I (and, by extension, you, if you’re interested) can have Solr do normalization during both index and search time.

Comments are closed.

A couple days ago I decided to finally get back to working on threach to try to deal with problems it had — essentially, it didn’t deal well with non-local exits due to calls to break or even something simple like a NoMethodError.

[BTW, I think I managed it. As near as I can tell, threach version 0.4 won't deadlock anymore]

Along the way, while trying to figure out how threads affect the behavior of different non-local exits, I noticed that in some cases there was still work being done by one or more threads long after there was an exception raised.

I re-discovered something that a lot of people already know: raise/rescue under MRI is slow, and under JRuby can be unbearably slow. How slow?

Let’s look at four simple blocks that exercise four different block exit strategies: break, catch and throw, raise with the normal single (or zero) arguments, as well as the three-argument version of raise.

Simple breakCatch/Throw
range.each do |i|      
  break          
end              
      
    
catch(:benchmarking) do  
 range.each do |i|      
   throw(:benchmarking) 
 end                    
end
      
    
Raise (1 arg)Raise (3 args)
 begin                  
   range.each do |i|    
     raise StandardError
   end                  
 rescue                 
  # do nothing                
 end                          
     
    
begin                  
  range.each do |i|
    raise StandardError, :hi, nil
  end
rescue 
 # do nothing
end
      
    

In each case, we immediately exit the block without doing any work; the idea is to measure how long it takes to break out for each case.

So....let's run them each 100K times and see what happens, shall we? Times are in seconds, averaged over two runs.

Ruby 1.8Ruby 1.9JRubyJRuby --1.9
break 0.120.070.29 0.21
catch/throw 0.350.280.64 0.48
raise (1 arg)1.782.1026.6022.06
raise (3 arg)1.852.130.45 0.45

The first thing to note is that this is 100K iterations. Three of the strategies are fast enough that you'd have to work really, really hard to notice them. In terms of speed, raise (3 args), catch/throw, and break are fast enough that you shouldn't bother worrying about them (although you should choose the method that makes your code easy to understand).

The second things to note is Holy Camoli! JRuby is slow there!

This Jira ticket tells the tale: The creation of the backtrace is very, very expensive for JRuby. That nil at the end of the raise (3 args) call suppresses the creation of that backtrace, so the speed is fine.

Three things worth saying here:

  • If you're using raise/rescue for flow control, you're already doing it wrong. Reserve exceptions for, well, exceptional conditions that are only going to be raised once or twice, not all the time.
  • If you're writing code that, for some ungodly reason, is planning on raising a crapload of exceptions, use the three-arg version. I'm looking at you, gem authors.
  • If you're writing your code without worrying about how it will work under multiple threads, well, please don't do that. Everyone has multi-core systems these days, and it's silly to not be able to use them. Plus, counting on Matz to never move to a VM with real threads is a big gamble.

4 Responses to “A short ruby diversion: cost of flow control under Ruby”

  1. For flow control in ruby, there’s actually a throw/catch architecture, which is an entirely different beast from raise/rescue. Nobody hardly ever uses them, throw/catch, I never see em, never used em myself either.

    Note: raise/rescue DO correspond to JAVA’s throw/catch. ruby’s throw/catch is something different: It can only be used in ‘static scoped’ situations, basically where the catch is in a static code block that’s a parent of the throw. But if people are using raise/rescue for ‘flow control’ scenarios in places where throw/catch would work…. would be interesting to benchmark the performance of throw/catch. throw/catch at least is indeed actually intended for flow control.

    Maybe nobody uses em cause they smell suspiciosuly like the dreaded ‘goto’, but that’s essentially what you’re doing with raise/rescue if you’re using em for flow control too, and apparently that doesn’t stop some people? Very curious what code you saw that was using raise/rescue like this, it’s certainly not a recommended thing to do by anyone (I don’t think?).

  2. PS: Am I the only one that never uses those raise syntactic sugar shortcuts? I always actually create the Exception object myself:

    raise StandardError.new

    “raise StandardError” does the same thing, it’s just a shortcut. And:

    raise StandardError, “message” ==== raise StandardError.new(“message”)

    I don’t know the way to avoid backtrace generation when throwing an actually explicitly created Exception object, but there probably is one.

  3. And briefly looking up the documentation on throw/catch, I’m wrong about the catch having to be statically scoped in a block above the ‘throw’ (the page I found in the online old ruby book actually specifically tells you this isn’t the case even though you might think it is, heh). But I’m still confused about where throw/catch can actually be used. It’s like the least used ruby language feature ever. But if lots of people are using raise/rescue for flow control, maybe throw/catch ought to be marketted better.

  4. Another blog figures out the same thing, posted on reddit. You beat them to it! http://www.coffeepowered.net/2011/06/17/jruby-performance-exceptions-are-not-flow-control/

ISBN parenthetical notes: Bad MARC data #1

April 12, 2011 at 12:22 pmCategory:Uncategorized

Yesterday, I gave a brief overview of why free text is hard to deal with.

Today, I’m turning my attention to a concrete example that drives me absolutely batshit crazy: taking a perfectly good unique-id field (in this case, the ISBN in the 020) and appending stuff onto the end of it.

The point is not to mock anything. Mocking will, however, be included for free.

What’s supposed to be in the 020?

Well, for starters, an ISBN (10 or 13 digit, we’re not picky).

Let’s not worry, for the moment, about the actual ISBN and whether it’s valid or not.

Wait, no, let’s go ahead and worry about it. It’s an easy enough script to write, although it takes a while to run.

8,630,794  Total records
3,220,666  Total 020a's
    6,498  020a's that don't obviously contain an ISBN
    8,407  that look like an ISBN but fail checksum test:
... so 0.26% of the ISBNs have invalid checksums

So, not bad at all, especially considering some of those are known to be bad, but are transcribed dutifully from the actual (mis-)printed book.

A lot of the malformed data (anything from which I can’t seem to extract something that looks like an ISBN) is pricing data, and most of it appears in system numbers that are close enough to each other that I presume it was just a bad batch.

What’s goes after the ISBN in the 020?

I’m no cataloger, of course, but it looks to me like the answer is “Something about how the book is bound together, or the publisher, unless you want to put something else there, and then, really, go ahead, because it’s not like anyone is ever going to want to parse this out, all we need to do is print cards with it for god’s sake.”

No, I kid, I kid! The actual rules are in Library of Congress Rule Interpretation 1.8, which reads, in part:

For a hardbound resource, there is no attempt to use a consistent term other than to use one that conveys the condition intelligibly.

I think it’s important to read that a second time, because it succinctly conveys the culture in which these rules were devised.

  • Don’t worry about consistency, because your only reader is human.
  • Defer to the cataloger.
  • Being complete is more important than being consistent.
  • Base your notes on your subjective view of the actual, physical item you’re presumed to be holding in your hands.

Interestingly (to me, anyway), it looks like the OCLC once had a (now deprecated) $$b subfield for binding information. Apparently it didn’t catch on.

What did I find?

So, let’s pretend I’d like to be able to differentiate between paperback and hardbound books. Probably useful, yes?

I went ahead and took all parenthetical notes from any field in the 020, split them on colon (’cause that seems to be the way they roll) and did some basic normalization:

  • Eliminate numbers (so ‘vol. 1′ and ‘vol. 2′ count as only one pattern)
  • Lowercase everything
  • Turn runs of spaces into a single space
  • Trim leading/trailing spaces
  • Remove any trailing punctuation

I found 1,506,729 parenthetical remarks in the 020 subfields of our catalog.

The top twenty most common entries using those normalizations are:

  1. 402537 pbk
  2. 387406 alk. paper
  3. 99260 v # (e.g., “v. 1″, “v. 22″, etc.)
  4. 82918 cloth
  5. 51125 hbk
  6. 42036 electronic bk
  7. 41360 acid-free paper
  8. 38792 hardcover
  9. 28913 set
  10. 20358 hardback
  11. 19160 ebook
  12. 16264 paper
  13. 15269 u.s
  14. 12770 hd.bd
  15. 11793 print
  16. 10625 lib. bdg
  17. 10520 hc
  18. 8772 est
  19. 7767 pb
  20. 7639 hard

The kicker? These are the top twenty of 13,374 unique parenthetical strings found in the 020 field. Many of them are publishers, or cities, or whatnot, but an awful lot of them are variations on “hardcover” and “paperback.”

For example, a quick search for anything that might be “hard” (regexp: /h[ar]{0,2}d/) got me started on a list. Here’s just the 90 examples from that list that start with ‘h’:

hard | hard adhesive | hard back | hard bd | hard book | hard bound | hard bound book | hard boundhard case | hard casehard copy | hard copy | hard copy set | hard cov | hard cover | hard covers | hard sewn | hard signed | hard-backhard-backcased | hard-bound | hard-cover | hard-cover acid-free | hardb | hard\cover | hardbach | hardback | hardback book | hardback cover | hardbackcased | hardbd | hardbk | hardbond | hardbook | hardboubd | hardbound | hardboundhardboundtion | hardc | hardcase | hardcopy | hardcopy publication | hardcov | hardcov er | hardcovcer | hardcove | hardcover | hardcover-alk. paper | hardcovercloth | hardcoverflexibound | hardcoverhardcoverwith cd | hardcoverr | hardcovers | hardcoversame | hardcoversame as above | hardcoverset | hardcovertion | hardcver | hardcvoer | hardcvr | harddback | harde | hardocover | hardover | hardpack | hardpaper | hardvocer | hardware | hd | hd bd | hd. bd | hd. bd. in slip case | hd. bd.in sl.cs | hd. bk | hd. cover | hd.bd | hd.bd. in box | hdb | hdbd | hdbk | hdbkb | hdbkhdbk | hdbnd | hdc | hdcvr | hdk | hdp | hdpk | hradback | hradcover | hrd | hrdbk | hrdcver | hrdcvr

And that’s after eliminating things like places of publication, strings like “with…”, “plus…”, “alk. paper”, etc.

“Yeah, but you have to understand that historically…”

Stop hiding behind that.

I understand that at one point in time it probably made sense (to someone at least) to do it this way. I can deal with that.

What I can’t accept is that as I type this there’s a cataloger doing this in this way. Today. April 2011. Some, what? maybe thirty years since computer-based OPACs became prevalent?

These sorts of problems were recognized ages ago and should have been dealt with. Add a subfield. Invent a controlled vocabulary. Don’t worry about the legacy data; it’s always going to suck.

But why are we still producing sucky data???

To sum up

The point is that there’s a better way to do this stuff. Lots and lots of better ways, in fact. Time I spend dealing with crappy data is time I don’t spend making relevancy raking better, or building a better command language search option for my librarians, or working on ways to get a decent “more like this”.

The need is both dire and urgent; the latter because sooner or later we’re going to have to go to a “two state solution” with traditional MARC21 for many of our records and whatever comes next (RDA?) for the newer stuff. And every day we wait, that first category grows, and the growth rate keeps increasing.

And then there’s serials. Don’t talk to me about serials.

4 Responses to “ISBN parenthetical notes: Bad MARC data #1”

  1. Chris says:

    Okay, so, what do you suggest we actually DO about this?

  2. Jakob says:

    What we should do is first making clear what parts of cataloging produce useless junk (like Bill did) and second clean up and normalize the data. A typical reason for avoidable quality problems is a lack of feedback. If you do not instantly get feedback about illformed data when you start to create a record, you will unlikely change your cataloging practice. So third we should create better cataloging clients. As long as cataloging rules are only written as rules instead of implemented as code that given error messages, I doubt that we get better data.

  3. Karen Coyle says:

    Bill, ISBN is one of the examples I use in my talks when I get to the point of “text versus data”. I have grabbed a couple of examples, but you’ve done a full-blown study, and I’m here to thank you for it! I will point people to this post for more info.

    Chris, as to what we do… given that any library system in existence today has algorithms to separate the ISBN from the rest of the subfield, we need to add a new subfield to MARC to hold the text and make that separate permanent. Actually, we need to have done that 20 years ago, and I almost feel like now it’s too late to make the change worthwhile since it looks like we’re on the verge of moving beyond MARC anyway.

  4. Matthew Phillips says:

    Yet another example of where the UKMARC format was superior. In UKMARC the ISBN was in subfield “a”, qualifying remarks in subfield “c” and price in subfield “d”. There was even a subfield “b” with a code to indicate the type of ISBN (e.g. whether it was the ISBN for a set of volumes or an individual volume).

    Sadly UKMARC was abandoned about ten years ago because it was just so unsatisfactory trying to convert from USMARC to UKMARC. It’s always easier to convert metadata into a format which is less expressive, so we all moved to USMARC instead.

One of the frustrating things about dealing with MARC (nee AACR2) data is how much nonsense is stored in free text when a unique identifier in a well-defined place would have done a much better job.

A lot of people seem to not understand why.

This post, then, is for all the catalogers out there who constantly answer my questions with, “Well, it depends” and don’t understand why that’s a problem.

Description vs Findability

I’m surprised — and a little dismayed — by how often I talk to people in the library world who don’t understand the difference between description and findability. AACR2 is clearly designed for description; once you’ve found a record, it does a pretty good job telling a human being what she’s looking at. With respect to a person who’s already got a copy of the record in her (virtual) hand, strings of text and reasonable abbreviations are…well, often good enough, let’s say.

But much of AACR2 is a giant mountain of fail when it comes to supporting findability — the ability for a machine to slice and dice the data in ways that can be mapped onto searches and transformations. What those of us on the business end of the computer need are well-defined values stuck into well-defined places that represent well-defined relationships.

Free text stuck on the end of a field fails all three of those criteria.

Machine Reasoning vs. Machine Parsing

When many people look at something like RDF, their first reaction is, “Great Googally Moogally! Just tell me the language! I don’t want to follow a chain of reasoning that’s seventeen steps long just to figure out the damn thing is in English!!!”

Of course you don’t. And you don’t have to. Someone — hopefully someone smarter than me — needs to write a program to do it. And we can.

Following all that logic — deriving relationships, figuring out eventual values, determining how to convert between various forms — is what I’ll call (for simplicity’s sake) machine reasoning. And machine reasoning — for the purposes of this discussion, anyway — is a solved problem. I’m not saying it’s not hard, and I’m not saying it might not take gobs of hardware resources. But we, the collective of humanity, know how to do it.

On the other hand, machine parsing — looking at all that free text that is sprinkled throughout our records and trying to turn it into something that is susceptible to machine reasoning — is vehemently not a solved problem. Even if you ignore all the misspellings, we’re still stuck with one-off abbreviations, lack of ordering, gobs of “local practice,” and iffy punctuation.

And, come to think of it, you can’t ignore the misspellings, either.

The point is this: good data trumps everything else. If there’s good, solid, well-defined data in computable places, we can (given some time) do damn near anything with it. If there’s human-entered, free-text, parenthetical-remark-type data, we’re pretty much stuck.

Examples?

Jonathan Rochkind just did a great post looking at LC call numbers, and how, well, they might be in a few different places, and may or may not be valid LC call numbers, and so on and on and on and on.

And my next post (hopefully tomorrow) will be an analysis of the first freetext in MARC I ever tried to deal with — the parenthetical remarks in the 020 (ISBN) field. If that doesn’t keep you up all night, well, I don’t know what will.

6 Responses to “Why programmers hate free text in MARC records”

  1. Chris says:

    I don’t think they don’t understand — I mean, the catalogers I deal with around here are pretty clever. I think that YOUR concerns are not THEIR concerns and they are working within the constraints of a system that has encouraged, and in some ways even forced, then to get cute with parens and abbreviations because no one was going to change anything to support their need to differentiate.

    The typos are of course only human, so I’m whatever on those. The punctuation, though? I cannot complain about enough. ;-)

  2. Chris says:

    You’ll note I demonstrated how okay I am with typos by inserting one. I did that on purpose, you know. No, really….

  3. Programmers hate MARC because it’s the cool thing to do.

    MARC records contain a lot more than the description. You don’t mention the subject metadata that most contain. This greatly enables retrieval (or findability). You do mention class numbers, which are also not part of the description and also help with retrieva.

    Also, there’s no reason to play findability and description off against each other. Each fills a different need.

  4. Cathy says:

    Just guesing here, since I’ve always been Colection Developmenet/Acquisitions, but haven’t those MARC records been pulled in from a lot of different sources, over many years, from the hands of many different catalogers? I remember something about the older a librar’s records are (the cataloging records), I mean the more hands that have worked on them, throught the years, a steady increase of mistakes or differences. Example: your search results featureing hard,hardback, etc, are possibly not even from the same library, originally. The fun of copy cataloging? Just a thought.

  5. Bill says:

    Jeffery, that’s a….let’s say perhaps an “overly broad” statement, and I’m not sure what you hope to add to the discussion with it. Programmers don’t, of course, hate MARC because “it’s the cool thing to do.” MARC-as-data-format is outdated and has a lot of flaws that are well-known. MARC-as-AACR2 as I’m treating it here is easy to hate because it’s full of description that could easily be made susceptible to machine parsing, but isn’t, and hence is at least partially wasted effort in that people are doing work that could be useful along both dimensions but isn’t.

Corrected Code4Lib slides are up

February 15, 2011 at 5:49 pmCategory:Uncategorized

…at the same URL.

I was, to put it mildly, incredibly excited about code4lib this year because, for once, I thought I had something to say. And I did have something to say. And I said it. But it was wrong.

I presented a bunch of statistics drawn from nearly a year of Mirlyn logs. The most outlandish of my assertions, and the one that eventually turned out to be the most incorrect, was that some 45% of all our user sessions consist of only one action: a search.

Unfortunately, I’d missed a whole swath of things I should have excluded. I’d remembered robots and stuff coming in from our link resolver and so on. I hadn’t counted on having to fight my own stupidity.

In short: catalog.hathitrust.org and mirlyn.lib.umich.edu share a common code base, as well as a Solr backend. I was correctly excluding all the HathiTrust stuff from my stats except for simple searches. What I ended up with was a whole lotta sessions with nothing in them but that search. Luckily, I noticed waaaay too many people coming in via the HathiTrust site (which I know doesn’t have a link to Mirlyn) and did more digging.

The slides have been updated with correct numbers. Luckily, even though the adjustment was pretty extreme, I don’t think many of my conclusions are invalidated, especially given corroborating evidence from an extensive survey conducted by our usability team (PDF). They conclude, among other things, that known-item searching is prevalent and relevancy raking is important across task boundaries.

The basic stats from the powerpoint, for those who don’t want to read all my notes:

  • 17% of all sessions have one action: a search
  • —In only 28% of all sessions does the user see the Record View
  • 75% of all logged actions that target an individual record (see the full record view, look at extended holdings, etc.) happen with a record in the top 6 search results
  • 7% of sessions involve a user adding a facet
  • 2% of sessions involve a user exporting records

2 Responses to “Corrected Code4Lib slides are up”

  1. Actually what this is is a really good lesson in the difficulty of getting valid results from usage statistics. “valid” meaning “actually answers the question you thought you were asking” in general.

    Doesn’t mean we shouldn’t look at usage statistics of course. But it takes care and time to get good numbers — and then more care and time to make sure the numbers actually allow you to draw the conclusions you want to draw.

    Not saying you didn’t do that here, but when my staff often asks me “Can’t we just get numbers to answer this question,” I will use this as an example of how you can often end up with numbers that don’t mean what you think they mean, and it takes non-trivial staff time to get and analyze the numbers to try to answer the questions you want to ask — so let’s be clear and intentional with the questions we’re asking, instead of just asking for ‘all the numbers’.