Sunday, January 10, 2016

Compressing an unsorted list of integers

You see a lot on offer for compressing sorted lists of integers, but not so much on the rather uncomfortable question of how to compress a list of unsorted numbers. So why would you want to do that? Surely all one needs to do is sort the list then compress it? The problem is that in many cases the order of the integers represents information that sorting would destroy. Here is my case in point.

Actual hit-positions in a search engine

In a search-engine you have to make a list of documents in which a term is found. Each document is assigned an identifier. So say we had a list of documents in which the word 'dog' occurs. It might be found in 7 documents: 0,1,1,2,2,3,4,6,11,21,21. We can use an algorithm like FastPFOR because the sorted list can be converted into a list of deltas, or the differences between successive entries. These will typically be much shorter numbers than the actual values. In my case an array of 100 document identifiers compressed down to just 13 integers. Cool. But what if you wanted to store the locations in those documents where the word 'dog' occurred as well? This would bloat the index considerably, since 'dog' might occur 100 times in a single document. I could sort and compress it the same way, but quite often it would be really short, maybe only one entry. Then the compression algorithm would actually increase the list size by three or four-fold, since the overhead for a compressed list adds a number of ints to the start, and – this is the real killer – one compressed list would be needed for each document the word was found in. So trying to compress it the conventional way would first, probably increase the overall index size, and second, you would need to maintain a lot of compressed lists.

The solution

Ideally, we would like just one list of word-positions for each word, just as we had one list of document identifiers for each word. But such a list would have to be unsorted, because if we sort it we would lose the information about which documents those positions refer to. Fortunately, most positions are quite small. They can't be greater then the length of the longest document the word is found in. Or if it was found in only a few documents, or always at the start, the values would be even smaller. Lets say that all the values are less than some amount like 127. (Convenient, huh?) Then the list could be stored in 8-bit integers with one 32-bit integer at the start to say how many bytes there were per integer. Or if 16 bits were needed, then we could use 2-byte ints, or 3 bytes for 24 bits etc. Worst case is when the documents are bigger than 4MB or so, when we will need 32-bit integers. But that's rare.

So the strategy is pretty simple. Scan all the numbers first to see what is the biggest (or smallest negative number) and work out how many bits we will need. I kind of cheat by rounding this up to the nearest 8 bits, but if you're interested you can refine it to have an arbitrary number of bits. But you won't gain much in compression and you will lose something in speed. Here's my Java code. It just uses ByteBuffer to build arbitrary-sized ints up to 32 bits, in 8-bit hops, and then stores the list as an array of 32-bit ints – compressed, of course. So typically what you'd expect is a 25%-50% reduction in the array size and in some cases 75%. Compressing it further by any significant amount seems to be impossible, given the near-random nature of the data.

I offer no guarantees that this works for all cases etc. But it is freeware. Do what you like with it.

Thursday, December 17, 2015

Tomcat 7 error

A few people seem to be bitten by an error in the Ubuntu distribution of Tomcat7. Basically when you shut down the service it gives the following messages:

Opinions differ on what causes it, but it is pretty clear: Tomcat is assuming that the shared, server and common directories, which are in /var/lib/tomcat7 are in /usr/share/tomcat7. In other words, the configuration has mixed up catalina.home with catalina.base. catalina.base is supposed to be /var/lib/tomcat7 and catalina.home is /usr/share/tomcat7. All you have to do is edit the file /var/lib/tomcat7/conf/catalina.properties so that all references to those directories have the correct prefix and you're good. I ignored the "common.loader" line as changing this created some weird effects. But lower down the file is mixed up. Reboot and you should be good.

Wednesday, November 4, 2015

Allow local File access via jQuery.ajax in Chrome/Chromium

It is often useful to encapsulate a website onto a local file system without using a web-server. I wanted to create a web-archive of a site and then substitute the jQuery get calls with local file reads. That way I would not need to access the Internet for the web-archive to work, and I could give it to other people on a usb stick, and they wouldn't have to install a webserver to run it. So I thought I'll use jQuery.get or jQuery.ajax to read the local file. They would all be JSON files, since that is what my server returns, but you can tweak it for other formats. After a bit of fiddling I got it right:

<!doctype HTML>
<html><head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<link id="link" rel="external" href="data.json">
<script src="jquery-1.11.3.min.js"></script>
<script>
$(document).ready(function(){
  $.ajax({
    url: "data.json",
    beforeSend: function(xhr){xhr.overrideMimeType("application/json");},
    dataType: "json",
    success: function(data){$("#result").html(data.text)}
  });
});
</script>
</head>
<body>
<p id="result"></p>
</body>
</html>

This reads the local file data.json and sets the contents of the #result element to the value of the "text" property. Here's my data.json just to be clear:

{"text": "Oh my word!"}

You also need a local copy of jQuery. Now this works just dandy on Safari and Firefox and I'm told in IE, but not in Opera or Chromium. Chromium says that this is a cross-origin request. I don't see why. I opened my HTML file using the file:// protocol and that tried to open a file in the same directory using the file:// protocol. How is that cross-origin? Because it made me cross? But the workarounds in Chromium/Chrome are dire: they suggest installing a local webserver – ridiculous. The only reason I am doing this is to avoid that. Or they say use the --allow-file-access-from-files option when launching chromium. But it doesn't work, at least not on Linux. However, I discovered that if you add --allow-file-access as a second option it works, though neither works on its own. I saved the options in /usr/share/applications/chromium.desktop under the Exec= property. It's a bit of a pain to ask people to do that but it is far better than installing a webserver.

Here's my Exec line from chromium.desktop:

Exec=chromium-browser --allow-file-access-from-files --allow-file-access %U

To fix Opera is more sensible: You just set Allow File XMLHttpRequest in UserPrefs of opera:config.

Saturday, July 4, 2015

Swapping suffixes on file names in bash

A common problem when writing shell scripts is to swap suffixes for file names. For example, I wanted to translate a batch of markdown files to html but also to apply a sed script so I could get curly quotes and long dashes etc. To do that I needed to create a temporary file, so I had to go from file.md to file.tmp to file.html. Each time I needed to swap suffixes. Having looked around I couldn't find a neat way to do that, and most of them used expr, which starts a new process. I wanted to do it natively in bash or even dash (the default Ubuntu shell). So I wrote a trivial but neat function and a test, which can be stripped out. The function is all you need:

#!/bin/bash
string="banana.md"
function swap {
    echo "${1:0:(${#1}-(${#2}+1))}.$3"
}
swap "banana.md" "md" "tmp"

To use it in a real script just use backticks thus:

...
function swap {
    echo "${1:0:(${#1}-(${#2}+1))}.$3"
}
markdown myfile.md > `swap "myfile.md" "md" "html"`
...

Sunday, June 14, 2015

Synchro-scrolling three or more columns

I wanted to make a display that had three parallel windows. The left one would show a succession of page-images of some source document; the middle one an editable transcription of the document's entire text content in a MarkDown-like language; the third a rendition of that text as HTML. This gave the user the same information in three forms that were intrinsically out of sync with one another, with each column having a different height and layout of information. As you scroll down one column you would naturally like the other column to scroll in sync, so that at some point on the page – say the middle – would contain the same stuff and so the user would not lose his/her way. One attempt can be seen at the ecdosis Web site. Keeping track of how far down each page-number in the textarea is, and the corresponding positions in pixels down the columns that correspond in the other two views is an implementation detail I'll leave to the reader, although my code is available at that site. More than likely, however, you'd want to do that your own way.

The feedback problem

The key problem with all such displays is this: if I scroll column 2, and then set the scrollTop attribute of the other two columns, this will generate new secondary scroll events for columns 1 and 3 that are indistinguishable from the original event. In jQuery you can test the event.originalEvent field of the scroll event but it is mostly set to true even when it isn't an original event. The result is uncontrollable feedback. The display can freeze as each column talks to each other. One scrolls it down, the other slightly up, setting it vibrating. You can use the jQuery.scroll method but you have to surrender control of the event feedback again. The result is choppy and not at all smooth.

My solution is simple. All you do is set some global flag to the name of the currently scrolling column. Initially this is undefined, but on first scrolling say column 2, the "textarea", the global var scroller = "textarea". Now in the scroll handlers for the other two columns all you do is test if the current value of scroller is that of the relevant scroll event handler. (Of course your code will be different. This is just an example):

The view clicked on will always scroll by itself and prevent feedback by blocking the secondary scroll events (the calls to the specialised self.scrollTo method in the code above) when the scroll did not originate there. At the completion of scrolling the global (actually self.scroller, a variable in the containing object) is set back to undefined after a 200 millisecond delay. The reason for this is that Javascript is asynchronous. We cannot assume that when the current scroll handler has finished that the other scrolls have finished as well. So we set a timeout function to delay the reset to ensure that it happens after all of the current scroll is complete. Any more than 200 milliseconds and the user may have tried to click on another panel and found it blocked:

The timeout id resets itself when the timeout has completed. This is also used to prevent timeouts accumulating as the user scrolls continuously.

Monday, May 25, 2015

The dreaded ssh "Roaming not allowed by server" problem

Passwordless login via ssh is a great technique. It allows you to turn off user challenge authentication altogether, and so shut out those robotic hackers who try to guess your user name and then try every password until they break in. With passwordless login they have to forge a long cryptographic RSA key which, given the number of possibilities and the latency on the line, is impossible. So when I went about restoring a server that had been hacked I put back my old ssh key, and tried to login. No joy. ssh -v mysite.com produced a mysterious ssh error: "Roaming not allowed by server". What does this mean, and how do you fix it? Googling the answer didn't help. No one seemed to know the answer. They were all fixated with file permissions, which may be an issue, but does it cause this error? Without reading through the open-ssh code here's what I found: delete .ssh/known_hosts on the client connecting to the server and all will be well. If the server's domain-name is dynamic, or has been altered (as in my case) then that counts as "roaming". When your IP address changes ssh will complain that a key in known_hosts has offended it. But when the server's address changes it will give you this "Roaming not allowed by server" message.

Friday, May 22, 2015

Next TEXT node in a HTML DOM

The Rangy tool is great for making cross-browser HTML selections. It has this useful function "surroundContents", which pastes in a span around the text of the selection. But it refuses to work if the selection crosses an element boundary. It's so stupid, because there is a perfectly sound way to wrap all the text elements in one selection, which is what I wanted. I was trying to implement a commenting tool, which adds a comment to an arbitrary selection in HTML. So the range needs to be coloured and have something to activate it when the user clicks on it. To do that I can't accept rangy's restriction on surroundContents. So I wondered if I could write a simple function that would wrap ANY text elements in the current selection with <span class="someclass">...</span>. It turns out it was pretty easy, although I couldn't find anything on the Web by searching. Here's my test code. If you check the HTML you'll see that it works when going up, down or across the DOM tree. According to the specs this works on any browser. This is a HTML DOM thing, so not much point converting it to jQuery. It should ideally be part of Rangy or jQuery, or converted to a jQuery plugin.