辞書アプリの改良:部分一致(4)

追記

Indexer.groovy、MetaIndex.groovyを修正(10/27: 01:54)

メタインデックス作成ツール修正

辞書アプリの改良:部分一致(2) - Random Noteで作成したメタインデックス作成ツールにはバグがあった。
Index#toMetaIndexListの中の下記の部分は

while (i <val.length()-MetaIndex.INDEX_KEY_CHAR_LENGTH -1) {

こうでないと、辞書インデックスの最後の3文字分がメタインデックス化されない。

while (i <val.length()-MetaIndex.INDEX_KEY_CHAR_LENGTH +1) {

ここを修正すると、今度はテストで使っている辞書データファイルのメタインデックスを作成するのに1GBのヒープメモリでも不足するようになってしまった。
そこで、まずはこのツールを修正する。
修正点は次の3つ。

  • 上記のバグフィックス
  • メタインデックス作成ツールと辞書アプリでクラスを共有するようになってきているので、共有しているクラスを別ファイルに分割
  • メタインデックス作成中に一時ファイルに作成中のメタインデックスを退避し、メモリの使用量を抑える

結果、できたソースは下記の通り。

Indexer.groovy

workDir = new File("work")
deleteTmpFile()
workDir.mkdirs()

def indexes = loadCompleteIndex()

List metaList = []
int i=0
for (idx in indexes) {
	metaList.addAll(idx.toMetaIndexList())
	
	i++
	if (i%100 == 0) {
		makeTmpFile(metaList)
		metaList = []
		println ("parsing:" + (i*100/indexes.size()) + "%")
	}
}
makeTmpFile(metaList)
makeMetaFile()

def makeMetaFile() {
	File outFile = new File("test.meta")
	outFile.delete()
	outFile.createNewFile()
	def os = new BufferedOutputStream(new FileOutputStream(outFile))
	listTmpFiles().each {f ->
			println ("writing: " + hex2str(f.name) + ":" + f.length() + ":" + (f.length() / MetaIndex.LENGTH))
			def buf = f.readBytes()
			def list = []
			def i = 0
			while ((i*MetaIndex.LENGTH) < buf.length) {
				list << new MetaIndex(buf, i*MetaIndex.LENGTH)
				i++
			}
			list.sort()
			i = 0
			for (m in list) {
				os.write(m.toBytes())
				metaSize += m.toBytes().length
				i++
			}
		}
	os.close()
}

def listTmpFiles() {
	def list = workDir.listFiles().toList()
	return list.sort {l,r ->
			return hex2str(l.name).compareTo(hex2str(r.name))
		}
}

def makeTmpFile(metaList) {
	metaList.sort()
	def prev = str2Hex("a")
	def tmpFile = new BufferedOutputStream(new FileOutputStream(new File(workDir, prev), true))
	for (m in metaList) {
		def first = str2Hex(m.word.substring(0,1))
		if (prev != first) {
			prev = first
			tmpFile.close()
			tmpFile = new BufferedOutputStream(new FileOutputStream(new File(workDir, prev), true))
		}
		tmpFile.write(m.toBytes())
	}
	tmpFile.close()
}

def str2Hex(String val) {
	byte[]  bytes = val.getBytes()
	def result = ""
	bytes.each {
		result += Integer.toHexString(it & 0xff)
	}
	return result
}
def hex2str(val) {
	byte[] buf = new byte[(val.length() / 2)]
	int i=0
	while (val.length() > 0) {
		buf[i] = Byte.parseByte(val.substring(0, 1), 16) * 16 + Byte.parseByte(val.substring(1, 2), 16)
		val = val.substring(2)
		i++
	}
	return new String(buf, "UTF-8")
}

def deleteTmpFile() {
	workDir.listFiles().each {
		it.delete()
	}
	workDir.delete()
}

def loadCompleteIndex(indexOffset = 0, indexLength = -1) {
	if (indexOffset < 0) indexOffset = 0
	RandomAccessFile randomFile = new RandomAccessFile("test.idx", "r")
	if (indexLength == -1) indexLength = randomFile.length()
	def buf = new byte[indexLength]
	randomFile.seek(indexOffset)
	randomFile.read(buf)
	randomFile.close()

	def i=0
	int offset = 0
	int count = 0
	def indexes = new ArrayList();
	while (i<buf.length) {
		if (buf[i] == 0) {
			def idx = new Index(buf, offset, i-offset+9)
			if (idx.word.length() > 0) indexes.add(idx)
			offset = i+9
			i = i+8
			count++
		}
		i++
		if (i%200000 == 0) println ("loading:" + (i*100/(indexLength)) + "%")
	}
	return indexes
}

BaseIndex.groovy

class BaseIndex {
	def word
	def offset
	def length

	def cut(bytes, o, l) {
		def i=0
		def buf = new byte[l]
		while (i < l) {
			buf[i] = bytes[i+o]
			i++
		}
		return buf
	}

	def byte2int(byte[] bytes) {
		return byte2int(bytes, 0, bytes.length)
	}
	def byte2int(byte[] bytes, int offset, int length) {
		def result = 0
		int i = 0
		while (i < length) {
			result += (bytes[i+offset] & 0xff) * (int)(Math.pow(256, length - i - 1))
			i++
		}
		return result
	}
	byte[] int2byte(intVal) {
		byte[] b = new byte[4];
		b[0] = ((intVal & 0xFF000000) /(256 ** 3))
		b[1] = ((intVal & 0x00FF0000) /(256 ** 2))
		b[2] = ((intVal & 0x0000FF00) /256)
		b[3] = intVal & 0x000000FF
		return b
	}
	int startsWith(String string) {
		int i=0
		while (i<word.length() && i<string.length()) {
			if (word.charAt(i) != string.charAt(i)) return word.charAt(i) - string.charAt(i)
			i++
		}
		return word.length() < string.length() ? -1 : 0
	}
	String toString() {
		return "{word: ${word},\toffset:${offset},\tlength:${length}}"
	}
	String normalize(String value) {
		return value.toLowerCase()
	}
}

Index.groovy

class Index extends BaseIndex {
	int indexOffset = 0
	int indexLength = 0
	def Index(byte[] buf, int o, int l) {
		word = new String(buf, o, l-9, "UTF-8").trim()
		offset = byte2int(buf, o+l-8, 4)
		length = byte2int(buf, o+l-4, 4)
		indexOffset = o
		indexLength = l
	}
	
	def toMetaIndexList() {
		List list = []
		def val = normalize(word)
		int i=0
		while (i <val.length()-MetaIndex.KEY_CHAR_LENGTH +1) {
			list << new MetaIndex(val.substring(i, i+MetaIndex.KEY_CHAR_LENGTH), indexOffset, indexLength, i)
			i++
		}
		if (val.length() == 1) list << new MetaIndex(val, indexOffset, indexLength, 0)
		return list
	}
}

MetaIndex.groovy

class MetaIndex extends BaseIndex implements Comparable {
	public static final int LENGTH = 24
	public static final int KEY_LENGTH = 12
	public static final int KEY_CHAR_LENGTH = 2
	
	int wordOffset = 0
	def MetaIndex(String w, int o, int l, int wo) {
		word = w
		offset = o
		length = l
		wordOffset = wo
	}
	def MetaIndex(byte[] buf) {
		parse(buf, 0)
	}
	def MetaIndex(byte[] buf, int o) {
		parse(buf, o)
	}
	def parse(byte[] buf, int o) {
		word = new String(buf, o, KEY_LENGTH, "UTF-8").trim()	// 余計なバイト(0)をtrimで削除
		offset = byte2int(buf, o+KEY_LENGTH, 4)
		length = byte2int(buf, o+KEY_LENGTH+4, 4)
		wordOffset = byte2int(buf, o+KEY_LENGTH+8, 4)
	}
	
	int compareTo(MetaIndex target) {
		if (word != target.word) return word.compareTo(target.word)
		if (offset != target.offset) return offset <=> target.offset
		if (wordOffset != target.wordOffset) return wordOffset <=> target.wordOffset
		return 0
	}
	int compareTo(Object target) {
		if (target instanceof MetaIndex) return compareTo((MetaIndex)target)
		if (target instanceof String) return word.compareTo(target)
		return -1
	}
	
	byte[] toBytes() {
		byte[] buf = new byte[LENGTH]
		Arrays.fill(buf, (byte)0)
		
		byte[] w = word.getBytes("UTF-8")
		byte[] o = int2byte(offset)
		byte[] l = int2byte(length)
		byte[] wo = int2byte(wordOffset)
		
		int i=0
		for (b in w) {
			buf[i] = b
			i++
		}
		i = 12
		for (b in o) {
			buf[i] = b
			i++
		}
		for (b in l) {
			buf[i] = b
			i++
		}
		for (b in wo) {
			buf[i] = b
			i++
		}
		return buf
	}
	String toString() {
		return "{word: ${word},\toffset:${offset},\tlength:${length},\twordOffset:${wordOffset}}"
	}
}