Most of the solutions posted thus far are either slow, or can yield spurious results in certain cases. (I haven't tested Picard, which I assume will work as intended. But personally I tend to recoil when I have to set up a JVM!)
Let's approach the problem from a different angle:
Good hash functions have ~O(1) complexity for a lookup. If we can create a hash table of our QNAMEs, then searching a BAM file should be O(n) in the number of records. The Python developers are very proud of the hash function in cpython, so let's use that:
#!/usr/bin/env python3
import sys
with open(sys.argv[1], 'r') as indexfile:
ids = set(l.rstrip('\r\n') for l in indexfile)
for line in sys.stdin:
qname, _ = line.split('\t', 1)
if qname in ids:
sys.stdout.write(line)
With a test set consisting of a ~3GB BAM file and a qnames.txt
with ~65K entries, running samtools view input.bam | ./idfilter.py qnames.txt
takes roughly 6.5 seconds on my test server. By comparison, piping the SAM to fgrep -f qnames.txt
(using GNU grep) takes about 8.5 seconds (but may yield spurious results).
(Why might grep
yield spurious results? If you have a QNAME foo
in your qnames.txt
, but your BAM file also contains fooX
and fooY
, they will all be matched by fgrep
. The solution is to anchor each of your search patterns between the beginning of the line and a tab, e.g. ^foo\t
, but then you have to use standard grep
which will have to construct an NFA for each pattern, and not fgrep
which implements Aho-Corasick, and you'll be orders of magnitude slower.)
I've rewritten my little Python script in Rust using std::collections::HashMap
for the hash function, and using the rust_htslib crate to read and write straight from BAM, and this is more than twice as fast as the Python, even when reading and writing BAM. However, my Rust isn't really fit for public consumption… 😬
samtools split
does. $\endgroup$