Showing posts with label java. Show all posts
Showing posts with label java. Show all posts

Monday, June 18, 2018

Google Java Format Pre-Commit Hook

My team decided to standardize on the Google Java Style Guide for formatting Java code; and not finding a drop-in git pre-commit hook for the Google Java Format library, I whipped one up and pushed it to GitHub as the Google Java Format Pre-Commit Hook project.

To use it, clone the repo, and link its pre-commit.sh script as the .git/hooks/pre-commit script in whatever project you want to use it with (or call it from your existing .git/hooks/pre-commit script, if you already have one). The script automatically downloads the Google Java Format library, and runs it over all staged .java files whenever you make a commit (and fails the commit if there are any formatting issues it can't automatically clean up).

You can skip the hook by including the --no-verify flag on an individual commit, or by setting the NO_VERIFY environment variable in your shell to be not empty prior to running a sequence of commits (like a merge or rebase). Full details for install and usage are in the project README.

Sunday, July 10, 2016

JPGPJ: A new Java GPG Library

The Bouncy Castle PGP implementation is the "standard" GPG/PGP library in Java, and it's quite solid — but it's cumbersome to use directly, since it pretty much forces you to learn and use the raw primitives of the OpenPGP spec (RFC 4880). Also, while there is some helpful example code in the Bouncy Castle examples package (and snippets from the same examples have been copied and pasted into a bunch of Stack Overflow answers), the example code is (appropriately?) cryptic, and covers only a limited subset of functionality in each example.

Encrypting with JPGPJ

So I wrote a small library, JPGPJ, to wrap the Bouncy Castle PGP implementation with a simple API for encrypting and decrypting files. It makes interoperating with the standard gpg command-line client (GnuPGP) a breeze. This is all you need to do to encrypt a file with Bob's public key, and sign it with Alice's private key:

new Encryptor(
    new Key(new File("path/to/my/keys/alice-sec.gpg"), "password123"),
    new Key(new File("path/to/my/keys/bob-pub.gpg"))
).encrypt(
    new File("path/to/plaintext.txt"),
    new File("path/to/ciphertext.txt.gpg")
);

The above Java code does the same thing as following gpg command (where Alice has an `alice` secret key and a `bob` public key on her keyring, and enters "password123" when prompted for her passphrase):

gpg --sign --encrypt --local-user alice --recipient alice --recipient bob \
    --output path/to/ciphertext.txt.gpg path/to/plaintext.txt

JPGPJ is set up to do the right thing by default — sign and encrypt — but if you just want to encrypt without signing, that's easy, too — just set the encryptor's signingAlgoritm property to Unsigned:

Encryptor encryptor = new Encryptor(
    new Key(new File("path/to/my/keys/bob-pub.gpg"))
);
encryptor.setSigningAlgorithm(HashingAlgorithm.Unsigned);
encryptor.encrypt(
    new File("path/to/plaintext.txt"),
    new File("path/to/ciphertext.txt.gpg")
);

To encode with ASCII Armor (ie produce Base64-encoded content, instead of binary content), just turn on the encryptor's asciiArmored flag:

Encryptor encryptor = new Encryptor(
    new Key(new File("path/to/my/keys/bob-pub.gpg"))
);
encryptor.setSigningAlgorithm(HashingAlgorithm.Unsigned);
encryptor.setAsciiArmored(true);
encryptor.encrypt(
    new File("path/to/plaintext.txt"),
    new File("path/to/ciphertext.txt.asc")
);

Decrypting with JPGPJ

Decrypting is just as easy. JPGPJ handles signed or unsigned, encrypted or unencrypted, compressed or uncompressed, ascii-armored or binary messages all the same way. Its default setting is to require messages to be signed by a known key; so for example, to decrypt a message signed by Alice's private key and encrypted with Bob's public key (requiring Alice's public key to verify and Bob's private key to decrypt), this is all the Java you need:

new Decryptor(
    new Key(new File("path/to/my/keys/alice-pub.gpg")),
    new Key(new File("path/to/my/keys/bob-sec.gpg"), "b0bru1z!")
).decrypt(
    new File("path/to/ciphertext.txt.gpg"),
    new File("path/back-to/plaintext.txt")
);

The above Java code does the same thing as the following gpg command (where Bob has a `bob` secret key and an `alice` public key on his keyring, and enters "b0bru1z!" when prompted for his passphrase):

gpg --decrypt --output path/back-to/plaintext.txt path/to/ciphertext.txt.gpg

If the message can't be verified by any known key (that is, any key with which the decryptor had been configured), JPGPJ will raise a VerificationException. If the message can't be decrypted by any known private key (that is, any private key with which the decryptor had been configured), JPGPJ will raise a DecryptionException.

To ignore signatures (in other words, decrypt a message successfully regardless of whether it was signed or not), simply turn off the decryptor's verificationRequired flag:

Decryptor decryptor = new Decryptor(
    new Key(new File("path/to/my/keys/bob-sec.gpg"), "b0bru1z!")
);
decryptor.setVerificationRequired(false);
decryptor.decrypt(
    new File("path/to/ciphertext.txt.gpg"),
    new File("path/back-to/plaintext.txt")
);

Keys in JPGPJ

The key data used by JPGPJ is simply what get when you export a key from GnuPG, like with the following gpg command for a public key:

gpg --export alice > path/to/my/keys/alice-pub.gpg

Or this gpg command to export a private key (which exports both the public and private parts of the key, encrypted with the same password that the key has on your GnuPG keyring):

gpg --export-secret-keys bob > path/to/my/keys/bob-sec.gpg

If you encode keys with ASCII Armor when you export them (via the GnuPG --armor flag), you can load them the same way in JPGPJ; and you can also embed ascii-armored keys as strings in your source code, if you find that more convenient than using external files (see the Key Rings wiki page for more details on loading and using keys in JPGPJ).

Saturday, May 14, 2016

Ubuntu 16.04 with Java 7 Timezone Data

As I found when upgrading to Ubuntu 16.04, Java 7 is no longer in the main Ubuntu repository — you have to install it via the OpenJDK PPA. That works nicely, but unfortunately this PPA doesn't include any timezone data.

In previous releases of Ubuntu, Java 6 and 7 timezone data were included via the tzdata-java package; but this package isn't available for Ubuntu 16.04. So I created a new tzdata-java PPA just for Ubuntu 16.04 (Xenial). You can install it like this:

sudo apt-add-repository ppa:justinludwig/tzdata
sudo apt-get install tzdata-java

To update my previous blog post: as a set of Ansible tasks, installing Java 7 on Ubuntu 16.04 now just works like this:

- name: register java 7 ppas
  become: yes
  apt_repository: repo={{ item }}
  with_items:
  - 'ppa:openjdk-r/ppa'
  - 'ppa:justinludwig/tzdata'

- name: install java 7 packages
  become: yes
  apt: pkg={{ item }}
  with_items:
  - openjdk-7-jdk
  - tzdata-java

Sunday, May 1, 2016

Xenial Ansible

I started building out some Ubuntu 16.04 (Xenial Xerus) servers this weekend with Ansible, and was impressed by how smoothly it went. The only major issue I encountered was that Ansible requires Python 2.x, whereas Ubuntu 16.04 ships Python 3.5 by default. Fortunately, it's not too hard to work around; here's how I fixed that — and a couple of other issues specific to the servers I was building out:

Python 2

Since Ansible doesn't work with Python 3, and that's what Ubuntu 16.04 provides by default, this is the error I got when I tried running Ansible against a newly-booted server:

/usr/bin/python: not found

So I had to make this the very first Ansible play (bootstrapping the ability of Ansible to use Python 2 for the rest of its tasks, as well as for its other record keeping — like gathering facts about the server):

- name: bootstrap python 2
  gather_facts: no
  tasks:
  - raw: sudo apt-get update -qq && sudo apt-get install -qq python2.7

And in the inventory variables (or group variables) for the server, I had to add this line (directing it to use Python 2 instead of the server's default Python):

ansible_python_interpreter: /usr/bin/python2.7

Aptitude

The next hiccup I ran into was using the Ansible apt module with the upgrade=full option. This option is implemented by using the aptitude program — which Ubuntu no longer installs by default. I was getting this error trying to use that option:

Could not find aptitude. Please ensure it is installed.

So I just tweaked my playbook to install the aptitude package first before running apt: upgrade=full:

- name: install apt requirements
  become: yes
  apt: pkg=aptitude

- name: update pre-installed packages
  become: yes
  apt: upgrade=full update_cache=yes

Mount with nobootwait

Then I started running into some minor issues that were completely unrelated to Ansible — simply changes Ubuntu had picked up between 15.10 and 16.04. The first of these was the nobootwait option for mountall (eg in /etc/fstab mountpoints). This option seems to be no longer supported — the server hung after rebooting, with this message in the syslog:

Unrecognized mount option "nobootwait" or missing value

Maybe this is just an issue with AWS EC2 instance-store volumes, but I had to change the /etc/fstab definition for the server's instance-store volume from this:

/dev/xvdb /mnt auto defaults,noatime,data=writeback,nobootwait,comment=cloudconfig 0 2

To this:

/dev/xvdb /mnt auto defaults,noatime,data=writeback,comment=cloudconfig 0 2

Java 7

The Ubuntu 16.04 repo no longer includes Java 6 or 7 — only Java 8 and 9. I got this error message trying to install Java 7:

No package matching 'openjdk-7-jdk' is available

So I first had to add a PPA for OpenJDK 7, and then could install it:

- name: register java 7 ppa
  become: yes
  apt_repository: repo=ppa:openjdk-r/ppa

- name: install java 7
  become: yes
  apt: pkg=openjdk-7-jdk

But the PPA doesn't include the timezone database, so the time formatting in our app was restricted to GMT. So I had to "borrow" the timezone data from Ubuntu 14.04:

- name: download tzdata-java
  get_url:
    url: http://mirrors.kernel.org/ubuntu/pool/main/t/tzdata/tzdata-java_2016d-0ubuntu0.14.04_all.deb
    dest: ~/tzdata-java.deb
    checksum: sha256:5131aa5219739ac58c00e18e8c9d8c5d6c63fc87236c9b5f314f7d06b46b79fb

- name: install tzdata-java
  become: yes
  command: dpkg --ignore-depends=tzdata -i tzdata-java.deb

That's probably going to be broken in a month or two after the next tzdata update, but it's good enough for now. Later I'll put together some Ansible tasks to build the data from source (although obviously the long-term solution is to upgrade to java 8).

Update 5/14/2016: I created a Xenial tzdata-java package to simplify the installation of Java 7 timezone data.

MySQL root

The final issue I hit was with the MySQL root user. Unlike previous versions of Ubuntu, which by default would come with 3 or 4 MySQL root users (covering all the different variations for naming your local host), and all with empty passwords, Ubuntu 16.04 comes with just a single root user — with which you can login only via MySQL's auth_socket plugin. So I was getting this error trying to login to MySQL as root the old way:

unable to connect to database, check login_user and login_password are correct or /home/me/.my.cnf has the credentials

The new way is simply to login to MySQL using the system's root user (ie sudo mysql). In Ansible tasks (such as those using the mysql_db module), this just means using the become (aka sudo) directive, and omitting the login_user and login_password options:

- name: create new database
  become: yes
  mysql_db: name=thedata

Sunday, February 22, 2015

Importing the New RDS CA Certificate Into the Java Keytool

A few days ago (Feb 2015), Amazon released a new CA certificate bundle (rds-combined-ca-bundle.pem) for use with AWS RDS databases. In order to connect with a MySQL, PostgreSQL, or SQL Server DB over SSL, your client app has to trust the certificates in this bundle. For java apps (using JDBC) this generally requires importing the certs into your java keystore via the java keytool utility.

The first RDS cert (from 2010) was released as a single certificate in a single file; but this new release is actually 10 separate new certificates (plus the old certificate) jammed into the same file. The java keytool, however, supports importing only one certificate at a time — so you have to first split up the bundle into individual certificates, and then import the certs one by one. If you try to import the whole bundle as a single file, you'll end up trusting only the first cert listed in that file (which happens, in this case, to be the old cert).

I wrote up a quick shell script that 1) downloads the new CA bundle, 2) splits up the bundle into individial certificate files, and 3) imports each file separately into the java keytool. It assumes that you already have the keytool installed and on your path; that the keystore location is /etc/ssl/certs/java/cacerts and the password is changeit (the defaults for the OpenJDK on Ubuntu); that you're running the script as a sudoer; and that you have OpenSSL installed. You'll have to tweak those things if that's not the case:

#!/bin/sh -e

# create a temp dir in which to work
OLDDIR="$PWD"
mkdir /tmp/rds-ca && cd /tmp/rds-ca

# download the bundle
wget https://s3.amazonaws.com/rds-downloads/rds-combined-ca-bundle.pem
# split the bundle into individual certs (prefixed with xx)
csplit -sz rds-combined-ca-bundle.pem '/-BEGIN CERTIFICATE-/' '{*}'

# import each cert individually
for CERT in xx*; do
    # extract a human-readable alias from the cert
    ALIAS=$(openssl x509 -noout -text -in $CERT |
        perl -ne 'next unless /Subject:/; s/.*CN=//; print')
    echo "importing $ALIAS"
    # import the cert into the default java keystore
    sudo keytool -import \
        -keystore /etc/ssl/certs/java/cacerts \
        -storepass changeit -noprompt \
        -alias "$ALIAS" -file $CERT
done

# back out of the temp dir and delete it
cd "$OLDDIR"
rm -r /tmp/rds-ca

# list the imported rds certs as a sanity check
keytool -list \
    -keystore /etc/ssl/certs/java/cacerts \
    -storepass changeit -noprompt |
    grep -i rds

OpenSSL, in particular, is used by the script only to extract a convenient, unique alias for each certificate; so if you don't have OpenSSL installed, you could replace the alias-creating line with something like this (which will create aliases in the form of rdsxx00, rdsxx01, etc):

    ALIAS=rds$CERT

Saturday, October 12, 2013

Java Thread Dumps for Daemons With jstack

jstack is a really helpful utility that comes standard with most linux JDK versions. It allows you to generate java thread dumps in situations where kill -3 won't work. kill -3 (aka kill -QUIT) will dump a java process's threads to stderr — but this, of course, works only when you still have access to stderr. If you're running a java process as a daemon (like a jetty or tomcat or jboss etc server), stderr usually is inaccessible.

Fortunately, jstack allows you to generate thread dumps without needing to read stderr from the original java process. jstack will dump the threads to jstack's own stdout, which you can pipe to a file, or through a pager, or just send directly to your terminial. There a few tricks to using it, however, which don't seem to be documented anywhere:

1. Use the right jstack executable

jstack usually will work only if it came with the same exact JDK version as the target JVM process is running. Since a lot of servers end up having several different JVM versions installed on them, it's important to make sure that the version of jstack you're trying to use is the right one — the jstack executable at /usr/bin/jstack won't necessarily be correct. And since jstack doesn't accept a -version flag, it's pretty hard to tell which version /usr/bin/jstack actually is.

So the most reliable way to run jstack is from the bin directory of the JDK which you're using to run the target JVM process. On ubuntu, this usually will be a subdirectory of one of the JDKs in the /usr/lib/jvm directory (like /usr/lib/jvm/java-6-openjdk-amd64 for the 64-bit version of the java 6 JDK). In that case, you might run jstack like this (when the target JVM's process ID is 12345):

/usr/lib/jvm/java-6-openjdk-amd64/bin/jstack 12345
2. Run jstack as the same user as the target JVM

You need to run jstack as the same user as which the target JVM is running. For example, if you're running jetty as a user named jetty, (and the jetty process ID is 12345) use sudo to execute jstack as the jetty user:

sudo -u jetty jstack 12345
(I learned this trick from Michael Moser's jstack - the missing manual blog post — apparently jstack uses a named pipe to communicate with the target JVM process, and that pipe's permissions allow only the user who created the target JVM process to read or write the pipe.)
3. Try, try again

Sometimes, even if you do those first two things, jstack will still tell you to go get bent (or some other inscrutable error message of similar intent). I've found that if I just try running it again a couple of times, jstack magically will work on the second or third try.

4. Don't use -F

Even though jstack sometimes itself will suggest that you try -F (particularly if you've got a version mismatch between jstack and the target JVM), resist the temptation to "force" it. When you use jstack with the -F option, jstack will actually stop the target process (ie kill -STOP). Only use the -F option if your app is already good and hung (because it certainly will be once you use -F).

Sunday, July 21, 2013

Tuning Lucene to Get the Most Relevant Results

Just spent the last week tuning our search engine using the latest version of Lucene (4.3.1). While Lucene works amazingly well right out of the box, to get "Google-like" relevancy for your results, you usually need to devise a custom strategy for indexing and querying the particular content your application has. Here are a few tricks we used for our content (which is English-only, jargon-heavy, and contains many terms used only by a few documents), plus some more basic techniques that just took us a while to figure out:

Use a custom analyzer for English text

Lucene's StandardAnalyzer does a good job generally of tokenizing text into individual words (aka "terms"), and it skips English "stopwords" (like the, a, etc) by default — but if you have only English text, you can get better results by using the EnglishAnalyzer. Beyond the tokenizing filters that the StandardAnalyzer includes, the EnglishAnalyzer also includes the EnglishPossesiveFilter (for stripping 's from words) and the PorterStemFilter (for chopping off common word suffixes, like removing ming from stemming, etc).

Because some of our text includes non-English names with letters not in the English alphabet (like é in liberté), and we know our users are going to want to search for those names using just English-alphabet letters, we implemented our own analyzer that included the ASCIIFoldingFilter on top of the filters in the regular EnglishAnalyzer. This filter converts characters not in the (7-byte) ASCII range to the ASCII characters that they resemble most closely; for example, it converts é to e (and © to (c), etc).

A custom analyzer is easy to implement; this is what ours looks like in java (the matchVersion and stopwords variables are fields from its Analyzer and StopwordAnalyzerBase superclasses, and the TokenStreamComponents is an inner class of Analyzer):

import java.io.Reader;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.Tokenizer;
import org.apache.lucene.analysis.core.LowerCaseFilter;
import org.apache.lucene.analysis.core.StopFilter;
import org.apache.lucene.analysis.en.EnglishPossessiveFilter;
import org.apache.lucene.analysis.en.PorterStemFilter;
import org.apache.lucene.analysis.miscellaneous.ASCIIFoldingFilter;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.analysis.standard.StandardFilter;
import org.apache.lucene.analysis.standard.StandardTokenizer;
import org.apache.lucene.analysis.util.StopwordAnalyzerBase;
import org.apache.lucene.util.Version;

public class CustomEnglishAnalyzer extends StopwordAnalyzerBase {

    /** Tokens longer than this length are discarded. Defaults to 50 chars. */
    public int maxTokenLength = 50;

    public CustomEnglishAnalyzer() {
        super(Version.LUCENE_43, StandardAnalyzer.STOP_WORDS_SET);
    }

    @Override
    protected TokenStreamComponents createComponents(String fieldName, Reader reader) {
        final Tokenizer source = new StandardTokenizer(matchVersion, reader);
        source.setMaxTokenLength(maxTokenLength);

        TokenStream pipeline = source;
        pipeline = new StandardFilter(matchVersion, pipeline);
        pipeline = new EnglishPossessiveFilter(matchVersion, pipeline);
        pipeline = new ASCIIFoldingFilter(pipeline);
        pipeline = new LowerCaseFilter(matchVersion, pipeline);
        pipeline = new StopFilter(matchVersion, pipeline, stopwords);
        pipeline = new PorterStemFilter(pipeline);
        return new TokenStreamComponents(source, pipeline);
    }
}

Note that when you use a custom analyzer for indexing, it's important to use the same (or least a similar) analyzer for querying (and vice versa). For example, the EnglishAnalyzer will tokenize the phrase it's easily processed into two terms: easili (sic) and process. If you index this text with the EnglishAnalyzer, searching for the terms it's, easily, or processed will find no results — you have to create the query using the same analyzer to make sure the terms for which you're querying are actually easili and process.

You can use Lucene's StandardQueryParser to build an appropriate query for you out of a phrase, using Lucene's fancy querying syntax; or you can simply tokenize the phrase yourself with the following code, and build the query out of it yourself:

import java.util.ArrayList;
import java.util.List;
import java.io.StringReader;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.en.EnglishAnalyzer;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.apache.lucene.util.Version;

...

    List<String> tokenizePhrase(String phrase) {
        List<String> tokens = new ArrayList<String>();
        TokenStream stream = new EnglishAnalyzer(Version.LUCENE_43).tokenStream(
            "someField", new StringReader(phrase));

        stream.reset();
        while (steam.incrementToken())
            tokens.add(stream.getAttribute(CharTermAttribute).toString());
        stream.end();
        stream.close();

        return tokens;
    }
Use a custom scorer

The results you get back from a query and their order are heavily influenced by a number of factors: the text you have in your index, how you've tokenized and stored the text in the different fields of your index, and how you structure the query itself. You can also influence the ordering of results to a lesser degree by using a custom Similarity class when you build your index.

Lucene's default similarity class uses some fancy math to score the terms in its index (see this Lucene Scoring tutorial for a simpler explanation of the scoring algorithm), and you'll probably want to tweak only one or two of those factors. We implemented our own custom Similarity class that completely ignores document length, and provides a bigger boost for infrequently-appearing terms:

import org.apache.lucene.index.FieldInvertState;
import org.apache.lucene.search.similarities.DefaultSimilarity;

public class CustomSimilarity extends DefaultSimilarity {

    @Override
    public float lengthNorm(FieldInvertState state) {
        // simply return the field's configured boost value
        // instead of also factoring in the field's length
        return state.getBoost();
    }

    @Override
    public float idf(long docFreq, long numDocs) {
        // more-heavily weight terms that appear infrequently
        return (float) (Math.sqrt(numDocs/(double)(docFreq+1)) + 1.0);
    }
}

Once implemented, you can use this CustomSimilarity class when indexing by setting it on the IndexWriterConfig that you use for writing to the index, like this:

import java.io.File;
import org.apache.lucene.analysis.en.EnglishAnalyzer;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.Version;

...

    void indexSomething() {
        EnglishAnalyzer analyzer = new EnglishAnalyzer(Version.LUCENE_43);
        IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_43, analyzer);
        config.setSimilarity(new CustomSimilarity());

        FSDirectory directory = FSDirectory.open(new File("my-index"));
        IndexWriter writer = new IndexWriter(directory, config);
        // ... index something ...
        writer.close();
    }
Build your own query

Probably the single biggest way we improved our "result relevancy" in the eyes of our users was to build our queries programmatically from a user's query input, rather than asking them to use Lucene's standard query syntax. Our algorithm for generating queries first expands any abbreviations in the query (not using Lucene, just using an in-memory hashtable of our own custom list of abbreviations); then it builds a big query consisting of:

  1. the exact query phrase (with a little slop), boosted heavily
  2. varying combinations of the terms in the query phrase, boosted according to the number of matching terms
  3. individual terms in individual fields (using the boost associated with those fields)
  4. individual terms with no boost

This querying strategy compliments our indexing strategy, which is to index a few important fields of each document separately (like "name", "keywords", etc) with boost added to those fields at index time; and then to index all the text related to each document in on big fat field (the "all" field) with no boost associated with it. The parts of the query that check for different terms appearing in the same document (#1 and #2 from the list above) rely on the "all" field; whereas the parts of the query that check in which fields the terms appear (#3 and #4) make use of the other, specially-boosted fields.

Doing it this way allows us to instruct Lucene to weight results that contain more matches of different terms (or the exact phrase) more heavily than results that simply match the same term many times; but also to weight matches in important fields (like "name" and "keywords") above matches from the general text of the document.

The actual query-building part of our code looks like this (I removed the abbreviation-expanding bits for simplicity, though). The fields argument is the list of custom fields to query; the defaultField argument is the name of the "all" field; and it uses the tokenizePhrase() method from above to split the phrase into individual words:

import java.lang.Math;
import java.util.List;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.PhraseQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;

...

    Query buildQuery(String phrase, List<String> fields, String defaultField) {
        List<String> words = tokenizePhrase(String phrase);
        BooleanQuery q = new BooleanQuery();

        // create term combinations if there are multiple words in the query
        if (words.size() > 1) {
            // exact-phrase query
            PhraseQuery phraseQ = new PhraseQuery();
            for (int w = 0; w < tokens.size(); w++)
                phraseQ.add(new Term(defaultField, words.get(w)));
            phraseQ.setBoost(words.size() * 5);
            phraseQ.setSlop(2);
            q.add(phraseQ, BooleanClause.Occur.SHOULD);

            // 2 out of 4, 3 out of 4, 4 out of 4 (any order), etc
            // stop at 7 in case user enters a pathologically long query
            int maxRequired = Math.min(tokens.size(), 7);
            for (int minRequired = 2; minRequired <= maxRequired; minRequired++) { 
                BooleanQuery comboQ = new BooleanQuery();
                for (int w = 0; w < tokens.size(); w++)
                    comboQ.add(new Term(defaultField, words.get(w)), BooleanClause.Occur.SHOULD);
                comboQ.setBoost(minRequired * 3);
                comboQ.setMinimumNumberShouldMatch(minRequired);
                q.add(comboQ, BooleanClause.Occur.SHOULD);
            }
        }

        // create an individual term query for each word for each field
        for (int w = 0; w < tokens.size(); w++)
            for (int f = 0; f < fields.size(); f++)
                q.add(new Term(fields.get(f), words.get(w)), BooleanClause.Occur.SHOULD);

        return q;
    }
Boost important fields when indexing

When we do the document indexing, we set the boost of some of the important fields (like "name" and "keywords", etc), as described above, while dumping all the document's text (including name and keywords) into the "all" field. Following is an example (in which we use our own customized FieldType so that we can configure the field with the IndexOptions that the result highlighter needs, discussed later). The toDocument() method translates some particular type of domain object to a Lucene Document, with appropriate "kewords", "name", "all", etc fields; it would be called by our indexing process (from the indexSomething() method above) for each instance of that domain type that we have in our system in order to create a separate document with which to index each domain:

import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.FieldType;
import org.apache.lucene.index.FieldInfo;

...

    protected static final FieldType TEXT_FIELD_TYPE = getTextFieldType();

    static FieldType getTextFieldType() {
        FieldType type = new FieldType();
        type.setIndexed(true);
        type.setIndexOptions(FieldInfo.IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS);
        type.setStored(true);
        type.setTokenized(true);
        return type;
    }

    Document toDocument(MyDomainObject domain) {
        Document doc = new Document();

        Field keywordsField = new Field("keywords", domain.keywords, TEXT_FIELD_TYPE);
        keywordsField.setBoost(3f);
        doc.add(keywordsField);

        Field nameField = new Field("name", domain.name, TEXT_FIELD_TYPE);
        nameField.setBoost(2f);
        doc.add(nameField);

        // ... other fields ...

        StringBuilder all = new StringBuilder().
            append(domain.kewords).append("\n").
            append(domain.name).append("\n").
            append(domain.text).append("\n").
            append(domain.moreText).append("\n").
            toString();
        Field allField = new Field("all", all, TEXT_FIELD_TYPE);
        doc.add(allField);

        return doc;
    }
Filter by date with a NumericRangeQuery

Many of our individual documents are relevant only during a short time period, with the exact start and end dates defined by the document. When we query for anything, we query against a specific day chosen by the user. In our Lucene searches, we implement this with a filter that wraps a pair of NumericRangeQuerys, querying the "startDate" and "endDate" fields (although a more common scenario in other applications, however, might be to have a single "publishedDate" for each document, and allow users to choose separate start and end dates against which to filter -- in that case, you'd use a single NumericRangeQuery). We index the "startDate" and "endDate" fields like this, using an integer field of the form 20010203 to represent a date like 2001-02-03 (Feb 3, 2001):

import org.apache.lucene.document.Document;
import org.apache.lucene.document.IntField;
import org.apache.lucene.document.FieldType;
import org.apache.lucene.index.FieldInfo;

...

    protected static final FieldType DATE_FIELD_TYPE = new FieldType();
    static {
        TEXT_FIELD_TYPE.setIndexed(true);
        TEXT_FIELD_TYPE.setIndexOptions(FieldInfo.IndexOptions.DOCS_ONLY);
        TEXT_FIELD_TYPE.setNumericType(FieldType.NumericType.INT);
        TEXT_FIELD_TYPE.setOmitNorms(true);
        TEXT_FIELD_TYPE.setStored(true);
    }

    Document toDocument(MyDomainObject domain) {
        Document doc = new Document();

        Field startField = new Field("startDate", domain.startDate, DATE_FIELD_TYPE);
        doc.add(startField);

        Field endField = new Field("endDate", domain.endDate, DATE_FIELD_TYPE);
        doc.add(endField);

        // ... other fields ...

        return doc;
    }

Then we build a filter like this, caching a separate filter instance per date (dates again represented in integer form like 20010203 to stand for 2001-02-03):

import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Filter;
import org.apache.lucene.search.NumericRangeQuery;
import org.apache.lucene.search.QueryWrapperFilter;

...

    synchronized protected Map cachedFilters = new HashMap();

    Filter getDateFilter(int date) {
        Filter filter = cachedFilters.get(date);

        if (filter == null) {
            BooleanQuery q = new BooleanQuery();

            // startDate must be on or before the specified date
            q.add(NumericRangeQuery.newIntRange(
                "startDate", 0, date, true, true
            ), BooleanClause.Occur.MUST);

            // endDate must be on or after the specified date
            // 30000000 represents the distant future (just prior to the year 3000)
            add NumericRangeQuery.newIntRange(
                "endDate", date, 30000000, true, true
            ), BooleanClause.Occur.MUST);

            filter = new QueryWrapperFilter(q);
            cachedFilters.put(date, filter);
        }

        return filter
    }
Use a SearcherManager for multi-threaded searching

To manage the access of multiple threads searching the index, Lucene provides a simple SearchManager class. Once the index has been created, you can instantiate it and call its acquire() method to check out a IndexSearcher instance.

We needed to initialize our IndexSearcher instances with our custom Similarity class (discussed above), so we initialized the manager with a custom SearcherFactory, which then allowed us to customize the IndexSearcher initialization process:

import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.SearcherFactory;

public class CustomSearcherFactory extends SearcherFactory {

    @Override
    public IndexSearcher newSearcher(IndexReader r) throws IOException {
        IndexSearcher searcher = new IndexSearcher(r);
        searcher.setSimilarity(new CustomSimilarity());
        return searcher;
    }
}

To use it, we create a SearcherManager instance when initializing the index (in the init() method) — note that the index must already exist before creating the SearcherManager; and then acquire and release the IndexSearcher it provides whenever we actually need to run a search on the index (in the search() method):

import java.io.File;
import org.apache.lucene.search.Filter;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.SearcherManager;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.FSDirectory;

...

    protected SearcherManager searchManager;

    protected init() {
        FSDirectory directory = FSDirectory.open(new File("my-index"));
        searchManager = new SearcherManager(directory, new CustomSearcherFactory());
    }

    public TopDocs search(Query query, Filter filter, int maxResults) {
        IndexSearcher searcher = searchManager.acquire();
        try {
            return searcher.search(query, filter, maxResults);
        } finally {
            searchManager.release(searcher);
        }
    }

After re-indexing, make sure to call maybeRefresh() on the SearchManager to refresh the managed IndexSearchers with the latest copy of the index. In other words, indexSomething() method from above would be finished like this:

    void indexSomething() {
        // ... index something ...
        writer.close();

        searchManager.maybeRefresh();
    }
Highlight results with a PostingsHighlighter

The PostingsHighlighter class is the newest implementation of a results highlighter for Lucene (the component that comes up with the fragments of text to display for each result in the search-results UI). It's only been part of Lucene since the 4.1 release, but our experience has been that it selects more-clearly relevant sections of the text than the previous highlighter implementation, the FastVectorHighlighter.

The first step to using a results highlighter is to make sure that you include at index time the data that the highlighter will need at search time. With the FastVectorHighlighter, we used this configuration for a regular indexed field:

import org.apache.lucene.document.FieldType;
import org.apache.lucene.index.FieldInfo;

...

    static FieldType getTextFieldType() {
        FieldType type = new FieldType();
        type.setIndexed(true);
        type.setIndexOptions(FieldInfo.IndexOptions.DOCS_AND_FREQS_AND_POSITIONS);
        type.setStored(true);
        type.setStoredTermVectorOffsets(true);
        type.setStoredTermVectorPayloads(true);
        type.setStoredTermVectorPositions(true);
        type.setStoredTermVectors(true);
        type.setTokenized(true);
        return type;
    }

But with the PostingsHighlighter, we found we didn't need to store the term vectors anymore — but we did need to index the term offsets:

import org.apache.lucene.document.FieldType;
import org.apache.lucene.index.FieldInfo;

...

    static FieldType getTextFieldType() {
        FieldType type = new FieldType();
        type.setIndexed(true);
        type.setIndexOptions(FieldInfo.IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS);
        type.setStored(true);
        type.setTokenized(true);
        return type;
    }

The PostingsHighlighter, by default, selects complete sentences to show. We have a lot of text that isn't in the form of proper sentences, however (much of our text isn't in the form of sentences begun with a captial letter and completed with a period and whitespace), so we subclassed the PostingsHighlighter with a class that uses a custom BreakIterator implementation that selects just a few words around each term to display.

With or without a custom BreakIterator, it's easy to use the PostingsHighlighter. You do need to have the IndexSearcher and TopDocs instance from the initial search results to use the PostingsHighlighter, so you might as well do both the search and the highlighting in the same method, returning the combined results in some intermediate data structure. For example, we can use a custom inner class called Result for each individual result, and combine one Lucene document object from the search results with the corresponding highlights text string from the highlighter in each returned Result:

import java.util.ArrayList;
import java.util.List;
import org.apache.lucene.document.Document;
import org.apache.lucene.search.Filter;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.postingshighlight.PostingsHighlighter;

...

    public class Result {
        public Document document;
        public String highlights;

        public Result(Document document, String highlights) {
            this.document = document;
            this.highlights = highlights;
        }
    }

    protected PostingsHighlighter highlighter = new PostingsHighlighter();

    public List<Result> search(Query query, Filter filter, int maxResults) {
        IndexSearcher searcher = searchManager.acquire();
        try {
            TopDocs topDocs = searcher.search(query, filter, maxResults);
            // select up to the three best highlights from the "all" field
            // of each result, concatenated with ellipses
            String[] highlights = highlighter.highlight("all", query, searcher, topDocs, 3);

            int length = topDocs.scoreDocs.length;
            List<Result> results = new ArrayList<Result>(length);
            for (int i = 0; i < length; i++) {
                int docId = topDocs.scoreDocs[i].doc;
                results.add(new Result(searcher.doc(docId), highlights[i]));
            }
            return results;

        } finally {
            searchManager.release(searcher);
        }
    }
With a tree, index leaves only

Some of our data is in hierarchical form, and we display the search results for that data in tree from. Rather than indexing all the nodes in the tree, however, we just index the leaves, and make sure that each leaf also includes the relevant text from its ancestors.

We also include the necessary info to render the leaf's branch as a separate, non-indexed "hierarchy" field in each leaf. When the leaf is returned as a search result, we build the branch out of that "hierarchy" field, and then merge the branches together to show each leaf in the context of the full tree.

This is the field configuration we use for the non-indexed "hierarchy" field:

import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.FieldType;
import org.apache.lucene.index.FieldInfo;

...

    protected static final FieldType NON_INDEXED_FIELD_TYPE = getNonIndexedFieldType();

    static FieldType getNonIndexedFieldType() {
        FieldType type = new FieldType();
        type.setIndexed(false);
        type.setOmitNorms(true);
        type.setStored(false);
        return type;
    }

    Document toDocument(MyDomainObject domain) {
        Document doc = new Document();

        // ... other fields ...

        String hierarchy = domain.getHierarchyText();
        Field allField = new Field("hierarchy", hierarchy, NON_INDEXED_FIELD_TYPE);
        doc.add(allField);

        return doc;
    }
Use a SpellChecker for auto-complete suggestions

For auto-complete suggestions in our application's search box, we created a custom search index of common words in our application domain that were at least six letters long, and used Lucene's SpellChecker class to index and search this word list. We skipped words less than six letters long to avoid suggesting simple words when the user has typed in only the first few letters of a word. To build the index, we created a plain text file with one word on each line, and indexed it with the following indexDictionary() method:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.PrefixQuery;
import org.apache.lucene.search.SearcherFactory;
import org.apache.lucene.search.SearcherManager;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.spell.PlainTextDictionary;
import org.apache.lucene.search.spell.SpellChecker;
import org.apache.lucene.search.spell.SuggestMode;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.Version;

public class Suggestor {

    File directory = new File("suggestion-index");
    SpellChecker spellChecker = new SpellChecker(FSDirectory.open(directory));

    public void indexDictionary(File dictionaryFile) {
        PlainTextDictionary dictionary = new PlainTextDictionary(dictionaryFile);
        IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_43,
            new StandardAnalyzer(Version.LUCENE_43));
        spellChecker.indexDictionary(dictionary, config, true);
    }
}

To then search it, we used a simple PrefxQuery for (partial) words less than 5 letters long; for longer words we used the SpellChecker's built-in fuzzy suggestion algorithm (with a 0.2f factor to make it even more fuzzy than the default). The suggestSimilar() method of our Suggestor class will return a list of up to 10 words appropriate as auto-completions for the partial word specified in the argument to suggestSimilar(). It delegates to helper prefixSearch() and fuzzySuggest() methods to actually run the search based on the length of the specified partial word:

    protected SearcherManager manager;

    protected getSearcherManager() {
        synchronized (directory) {
            if (manager == null)
                manager = new SearcherManager(
                    FSDirectory.open(directory), new SearcherFactory());
            return manager;
        }
    }

    public List<String> suggestSimilar(String s) {
        // search with prefix query if less than 5 chars
        // otherwise use spellChecker's built-in fuzzy suggestions
        return s.length() < 5 ? prefixSearch(s) : fuzzySuggest(s);
    }

    protected List<String> prefixSearch(String s) {
        SuggestionManager manager = getSearcherManager();
        IndexSearcher searcher = manager.acquire();
        try {
            // search for the top 10 words starting with s
            Term term = new Term("word", s.toLowerCase())
            TopDocs topDocs = searcher.search(new PrefixQuery(term), 10);

            int length = topDocs.scoreDocs.length;
            List<String> results = new ArrayList<String>(length);
            for (int i = 0; i < length; i++) {
                int docId = topDocs.scoreDocs[i].doc;
                results.add((searcher.doc(docId).get("word"));
            }
            return results;
        } finally {
            manager.release(searcher);
        }
    }

    protected List<String> fuzzySuggest(String s) {
        // search for 10 most popular words not exactly matching s
        String[] similar = spellChecker.suggestSimilar(
            s.toLowerCase(), 10, null, null,
            SuggestMode.SUGGEST_MORE_POPULAR, 0.2f);
        List<String> results = Arrays.asList(similar);

        // include queried term if it is itself a recognized word
        if (spellChecker.exist(term) {
            if (results.isEmpty())
                results.append(term);
            else
                results.set(results.size() - 1, term);
        }

        return results;
    }