Showing posts with label luke. Show all posts
Showing posts with label luke. Show all posts

Friday, November 19, 2010

Scripting Luke with Javascript

For quite some time now, I have been looking for a good way to build scripts that (a) allow people to "look inside" Luke indexes on a remote machine, and (b) can be embedded in larger scripts which run automatically via cron.

My first attempt was to write Python scripts with PyLucene, but it ended up never getting used, because our ops team prefer bash, and because of the effort of installing PyLucene on the production Unix boxes. PyLucene development has picked up again recently, but there was quite a long period during which we were using Lucene 2.x and PyLucene was only available for Lucene 1.x, so probably it was not such a bad thing.

My next attempt was with Lucli, the Lucene-CLI tool - I added a bunch of functionality to it, including the ability to run "lucli macros" (you can find the code here if you are interested). That never caught on with our scripting folks, however, probably because of the added complexity of having to maintain additional ".lucli" macro files in the repository - the preferred approach seems to be to send the commands into Lucli with a here document and parse the results with awk. Since we were not using the extra functionality of the local Lucli, when the time came to upgrade to Lucene 3.x, we simply pointed to the new JARs and it was business as usual.

Nowadays, when I need to do quick one-off analysis/debugging of Lucene indexes, I just use Jython and copy-paste from one of my older scripts. Not too different from writing Java code (ie we don't get PyLucene's Pythonic interface) but slightly more concise and easier to run from the command-line.

When I need to "look inside" a Lucene index on a remote machine, I ssh in with the -X option, then run Luke against the index on the remote machine. This points the DISPLAY on the remote machine to that of my local machine, so Luke shows up on my computer. The sequence of commands goes like this:

1
2
3
sujit@cyclone:~$ ssh -X sujit@avalanche
sujit@avalache's password: xxxx
sujit@avalanche:~$ luke.sh -index /path/to/index -ro

However, I recently downloaded Luke 1.0.1, and discovered that it came with a Javascript scripting console. It also takes a -script /path/to/script.js parameter on its command line, which got me all excited about the possibility of merging requirements (a) and (b) above into a single tool, and running against a codebase that (historically at least) has been faithfully tracking Lucene releases. Here's a screenshot:

However, a little testing showed that all the -script parameter does run the script within Luke's Javascript console - intuitively, the behavior I was expecting was for Luke to run the script, dump the results to STDOUT, and exit. I have an Issue open on Luke's Issue Tracker - feel free to vote for it if you agree.

Assuming that the above expectation is reasonable, and at some point in the future the -script parameter will behave as I think it should, I set about trying to figure out what I could do with the Javascript console. Here are some of the operations that I would use the scripting interface for:

Operation Comment
count([query]) If no query string is supplied, should return the number of records in the index. If query string is supplied, then it should return the number of matched records.
search(query) Execute the search specified by the query, and return the results.
find(fieldname, fieldvalue) Reads the index sequentially, returning documents where fieldname = fieldvalue.
get(docid) Return the document by docId
terms([fieldname]) Returns a map of all field names and their counts if no field name is specified. If fieldname is specified, then returns only the counts for this field name.

Javascript is not my favorite scripting language, and neither am I very good at it, but since it appears to be quite popular as an embedded scripting engine for Java-based apps (Alfresco and now Luke), I figured it was worth learning, and this would be a good opportunity. Here are the Javascript functions corresponding to the operations listed above. The ones prepended with an underscore are "private" functions used by the "public" (ie corresponding to an operation) functions..

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
function _is_undefined(value) {
  return typeof(value) === "undefined";
}

function _get_query(q) {
  var analyzer = 
    new Packages.org.apache.lucene.analysis.standard.StandardAnalyzer(
    app.getLuceneVersion());
  var parser = new Packages.org.apache.lucene.queryParser.QueryParser(
    app.getLuceneVersion(), "f", analyzer);
  return parser.parse(q);
}

function count() {
  print("count:" + ir.numDocs());
}

function count(q) {
  var searcher = new Packages.org.apache.lucene.search.IndexSearcher(ir);
  var query = _get_query(q);
  var hits = searcher.search(query, 100).scoreDocs;
  print("count(" + q + "):" + hits.length);
}

function search(q) {
  var searcher = new Packages.org.apache.lucene.search.IndexSearcher(ir);
  var query = _get_query(q);
  var hits = searcher.search(query, 100).scoreDocs;
  for (var i = 0; i < hits.length; i++) {
    get(hits[i].doc, hits[i].score);
  }
  searcher.close();
}

function find(key, val) {
  var numDocs = ir.numDocs();
  for (var i = 0; i < numDocs; i++) {
    var doc = ir.document(i);
    var docval = String(doc.get(key));
    if (docval == null) {
      continue;
    }
    if (val == docval) {
      get(i);
    }
  }
}

function get(docId, score) {
  if (_is_undefined(score)) {
    print("-- docId: " + docId + " --");
  } else {
    print("-- docId:" + docId + " (score:" + score + ") --");
  }
  var doc = ir.document(docId);
  var fields = doc.getFields();
  for (var i = 0; i < fields.size(); i++) {
    var field = fields.get(i);
    var fieldname = field.name();
    print(fieldname + ":" + doc.get(fieldname));
  }
}

function terms(fieldname) {
  var te = ir.terms();
  var termDict = {};
  while (te.next()) {
    var fldname = te.term().field();
    if (_is_undefined(termDict[fldname])) {
      termDict[fldname] = 1;
    } else {
      termDict[fldname] = termDict[fldname] + 1;
    }
  }
  if (fieldname == "") {
    var sortable = [];
    for (var key in termDict) {
      sortable.push([key, termDict[key]]);
    }
    var sortedTermDict = sortable.sort(function(a,b) { return b[1] - a[1]; });
    for (var i = 0; i < sortedTermDict.length; i++) {
      print(sortedTermDict[i][0] + ":" + sortedTermDict[i][1]);
    }
  } else {
    if (_is_undefined(termDict[fieldname])) {
      print("Field not found:" + fieldname);
    } else {
      print(fieldname + ":" + termDict[fieldname]);
    }
  }
}

// unit tests
print("#-docs in index");
count();
print("#-docs for title:bone");
count("title:bone");

print("Search for title:bone");
search("title:bone");

print("get doc 0");
get(0);

print("Find record with title: Broken bone");
find("title", "Broken bone");

print("printing all term counts");
terms("");
print("printing term counts for idx");
terms("idx");
print("printing term counts for non-existent field foo");
terms("foo");

The functions are pretty basic at the moment, I would want to be able to plug in custom analyzers and less frequently custom similarity implementations, and (even less frequently) custom sorts to the search function. But this can be easily accomplished by passing in extra parameters into the search function and a little bit of extra code.

I was unable to get a reference to the Version enum from within Javascript, so I had to add a new method getLuceneVersion() in Luke.java (so its now accessible as app.getLuceneVersion() from Javascript). It seems a reasonable thing to do since specific versions of Luke do track specific versions of Lucene. I added this method in and ran "ant dist" to rebuild the JARs so my shell script (see below) could find it.

1
2
3
  public Version getLuceneVersion() {
    return Version.LUCENE_30;
  }

To call Luke, I created a luke.sh file in luke-1.0.1 bin subdirectory.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/bin/bash
# Source: Downloads/luke-1.0.1/bin/luke.sh
BASEDIR=/Users/sujit/Downloads/luke-1.0.1
export CLASS_PATH=\
$BASEDIR/lib/hadoop/commons-cli-1.2.jar:\
$BASEDIR/lib/hadoop/commons-codec-1.3.jar:\
$BASEDIR/lib/hadoop/commons-httpclient-3.0.1.jar:\
$BASEDIR/lib/hadoop/commons-logging-1.0.4.jar:\
$BASEDIR/lib/hadoop/commons-logging-api-1.0.4.jar:\
$BASEDIR/lib/hadoop/commons-net-1.4.1.jar:\
$BASEDIR/lib/hadoop/ehcache-1.6.0.jar:\
$BASEDIR/lib/hadoop/hadoop-0.20.2-core.jar:\
$BASEDIR/lib/hadoop/jets3t-0.6.1.jar:\
$BASEDIR/lib/hadoop/kfs-0.2.2.jar:\
$BASEDIR/lib/hadoop/log4j-1.2.15.jar:\
$BASEDIR/lib/hadoop/oro-2.0.8.jar:\
$BASEDIR/lib/hadoop/slf4j-api-1.4.3.jar:\
$BASEDIR/lib/hadoop/slf4j-log4j12-1.4.3.jar:\
$BASEDIR/lib/hadoop/xmlenc-0.52.jar:\
$BASEDIR/lib/js.jar:\
$BASEDIR/lib/lucene-analyzers-3.0.1.jar:\
$BASEDIR/lib/lucene-core-3.0.1.jar:\
$BASEDIR/lib/lucene-misc-3.0.1.jar:\
$BASEDIR/lib/lucene-queries-3.0.1.jar:\
$BASEDIR/lib/lucene-snowball-3.0.1.jar:\
$BASEDIR/lib/lucene-xml-query-parser-3.0.1.jar:\
$BASEDIR/dist/luke-1.0.1.jar
java -cp $CLASS_PATH org.getopt.luke.Luke $*

During development, I edited the functions inside a single test.js file outside Luke (the Javascript console does not have command history, so it is not the best place to do development). Then I call Luke once as follows:

1
sujit@cyclone:luke-1.0.1$ bin/luke.sh -index /path/to/index -ro

And then in the Javascript console, the full test.js file can be loaded up with a load("/path/to/test.js"); and it would run the whole thing.

For regular use, one could write a (bash, although I would prefer Python) script that takes the inputs such as path to index, query string, etc, as command line parameters, then creates a temporary file that imports the function definitions and builds and appends the function call to make (similar to my unit tests) at the end of this temporary file. It would then launch Luke with the -script option pointing to this temporary file, which would run the script, output its results to STDOUT, and exit. The script would then parse the output (for downstream use) or return it as-is.

Thinking about this some more, though, it does seem like a lot of work and a lot of complexity. The main advantage of this approach is that you can probably stick with bash scripting, delegating to Luke for the Lucene stuff. However, that aside, now that PyLucene is an official Apache Lucene subproject, and one can be reasonably certain that it too, will track Lucene releases as faithfully as Luke does, it may be time to just dust off the old PyLucene based Python scripts and keep things simple.

Saturday, June 23, 2007

PyLucene: Python scripting for Lucene

I started learning Python about 3 years ago, and since then I have been trying to adapt it for all my scripting needs. Since I mostly do Java programming, I am not exactly what you would call a hardcore Python programmer. I find myself using Python mostly for database reporting, converting files of data from one format to another, etc. There have been times in the past when I would have to report on a Lucene index, or do some post-processing on an existing index to inject special one-off values on an index created by our index building pipeline, but my approach had been to simply write a Java program to do this. Since I dislike running Java programs from the command prompt (mainly because I have to write a shell script that sets the CLASSPATH), I end up writing a JUnit unit test to run the code. A lot of work, I know, but thats what I had to work with then.

I had read about PyLucene in the Lucene in Action book, but hadn't had the opportunity to actually download it and take it for a spin. This opportunity came up recently, and I am happy to report that installing and working with PyLucene was relatively painless and quite rewarding. In this post, I explain how I installed PyLucene on my Linux box and show two little scripts that I converted over from Java. From what I have seen, PyLucene has a strong following, but unlike me, these guys actually use PyLucene to build full fledged applications, not just little one-off scripts. Hopefully, once you see how simple it is, you will be encouraged to use it, even if you use a language such as Java or C# for mainline development.

PyLucene installation (Fedora Core 4 Linux)

The installation is relatively straightforward, but the instructions are not very explicit. I was trying to install on a box running Fedora Core 4 Linux, and there is no RPM package. Neither is there a package that can be installed by the standard "configure, make, make install" procedure. Seeing no pre-built packages for my distribution, I initially attempted to install from source, but ran into strange prompts that I could not answer, so I tried downloading the Unix binary distribution instead. I ended up copying the files from the binary distribution to my filesystem according to the README file included in this distribution.

1
2
3
4
5
6
sujit@sirocco:~/PyLucene-2.0$ ls
CHANGES  CREDITS  python  README  samples  test
sujit@sirocco:~/PyLucene-2.0$ cd python
sujit@sirocco:~/PyLucene-2.0/python$ ls
PyLucene.py  _PyLucene.so  security
sujit@sirocco:~/PyLucene-2.0/python$ cp -R /usr/lib/python-2.4/site-packages

Basically, I copied all the files under the python subdirectory of the downloaded binary distribution to my Python site-packages directory. That was the end of the installation.

To test this module, I decided to port the two Java programs I had written to do the simple index reporting and post-processing I spoke of earlier. Not only did they end up taking fewer lines of code to write, they are also at the right level of abstraction, since these things really deserve to be scripts. I also ended up setting up the groundwork to be able to build quick and dirty scripts to access and modify Lucene databases, just like I have for databases.

Script to report on crawled URLs in an Index

The script below just opens up an index whose directory is supplied on the command line, and returns a pipe-delimited report (which currently goes to stdout) of title and url. This can be useful for testing, since you will now know what kind of search term to enter for these indexes to come back with results. It can also be useful for verifying that we crawled the sites we were supposed to crawl.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/usr/bin/python
# Takes an index directory from the command line and produces a pipe
# delimited report of title and URL from the index.
import sys
import string
from PyLucene import IndexSearcher, StandardAnalyzer, FSDirectory

def usage():
  print " ".join([sys.argv[0], "/path/to/index/to/read"])
  sys.exit(-1)

def main():
  if (len(sys.argv) != 2):
    usage()
  path = sys.argv[1]
  dir = FSDirectory.getDirectory(path, False)
  searcher = IndexSearcher(dir)
  analyzer = StandardAnalyzer()
  numdocs = int(searcher.maxDoc())
  print "#-docs:", numdocs
  for i in range(1, numdocs):
    doc = searcher.doc(i)
    title = doc.get("title")
    url = doc.get("url")
    print "|".join([title.encode('ascii', 'replace'), url])
  searcher.close()

if __name__ == "__main__":
  main()

Script to inject additional precomputed data

This script takes a pre-built index as input and injects an additional field in some of the records depending on the URL. This can be useful if you set up your url field to be storable but do not tokenize it, so you may want to post process the index to match the URLs against one or more patterns and add in another facet field which you can then query on. In this case, the facet is set up as Index.UN_TOKENIZED so our application code will have to specify the exact facet its looking for.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/usr/bin/python
# Copies the index whose source directory is specified and copies it after
# transformations to the specified target directory. In this case, it looks
# at the URL and adds in a facet field.
import sys
import string
from PyLucene import IndexSearcher, IndexWriter, StandardAnalyzer, FSDirectory, Field

def usage():
  print " ".join([sys.argv[0], "/path/to/index/source", "/path/to/index/target"])
  sys.exit(-1)

def main():
  if (len(sys.argv) != 3):
    usage()
  srcPath = sys.argv[1]
  destPath = sys.argv[2]
  srcDir = FSDirectory.getDirectory(srcPath, False)
  destDir = FSDirectory.getDirectory(destPath, True)
  analyzer = StandardAnalyzer()
  searcher = IndexSearcher(srcDir)
  writer = IndexWriter(destDir, analyzer, True)
  numdocs = int(searcher.maxDoc())
  for i in range(1, numdocs):
    doc = searcher.doc(i)
    title = doc.get("title")
    url = doc.get("url")
    if (url.find("pattern1") > -1):
      doc.add(Field("facet", "pattern1", Field.Store.YES, Field.Index.UN_TOKENIZED))
    writer.addDocument(doc)
  searcher.close()
  writer.optimize()
  writer.close()

if __name__ == "__main__":
  main()

In both cases, the code should look familiar if you have worked with Lucene before. It is really the same Java classes wrapped up to be accessible through Python, so the only difference is the more compact Pythonic syntax. The one caveat is that PyLucene uses Lucene 1.4, whereas most Lucene shops are probably up at 2.0 or 2.1 (if you want to be on the bleeding edge). However, for one off scripts, the version difference should not make a difference most of the time, unless you are trying to use one of the newer features in your Python code.

Adding your own Analyzer to Luke

On a kind of related note, I was able to add Analyzers to my Luke application. I know support exists for this, and most Lucene programmers probably know how to do this already, but since there is no clear instructions on how to do this, I figured I'd write it up here. It's not hard once you know how. The standard shell script invocation for Luke is:

1
2
#!/bin/bash
java -jar $HOME/bin/lukeall-0.7.jar

I was experimenting with the Lucene based spell checker described in the Java.net: Did You Mean: Lucene? article, and I wanted to use the SubwordAnalyzer within Luke. Luke comes with a pretty comprehensive set of Analyzer implementations, but this one was not one of them. So I changed the script above to include the jar file that contained this class, along with its dependencies (such as commons-lang, commons-io, etc), and changed the java call to use -cp instead. Here is my new script to call Luke.

1
2
3
4
5
6
7
#!/bin/bash
M2_REPO=$HOME/.m2/repository
export CLASSPATH=$HOME/projects/spellcheck/target/spellcheck-1.0-SNAPSHOT.jar:\
  $M2_REPO/log4j/log4j/1.2.12/log4j-1.2.12.jar:\
  $M2_REPO/commons-io/commons-io/1.2/commons-io-1.2.jar:\
  $M2_REPO/commons-lang/commons-lang/2.2/commons-lang-2.2.jar
java -cp lukeall-0.7.jar:$CLASSPATH org.getopt.luke.Luke

And now I can use the SubwordAnalyzer from within Luke to query an index which used this analyzer to build an index out of a list of English words.