blob: d0b92476e9f8aafad92fb934c510934b2320055a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.search.suggest.tst;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import org.apache.lucene.search.suggest.InputIterator;
import org.apache.lucene.search.suggest.Lookup;
import org.apache.lucene.search.suggest.SortedInputIterator;
import org.apache.lucene.store.DataInput;
import org.apache.lucene.store.DataOutput;
import org.apache.lucene.store.Directory;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.CharsRefBuilder;
import org.apache.lucene.util.RamUsageEstimator;
/**
* Suggest implementation based on a
* <a href="http://en.wikipedia.org/wiki/Ternary_search_tree">Ternary Search Tree</a>
*
* @see TSTAutocomplete
*/
public class TSTLookup extends Lookup {
TernaryTreeNode root = new TernaryTreeNode();
TSTAutocomplete autocomplete = new TSTAutocomplete();
/** Number of entries the lookup was built with */
private long count = 0;
private final Directory tempDir;
private final String tempFileNamePrefix;
/**
* Creates a new TSTLookup with an empty Ternary Search Tree.
* @see #build(InputIterator)
*/
public TSTLookup() {
this(null, null);
}
/**
* Creates a new TSTLookup, for building.
* @see #build(InputIterator)
*/
public TSTLookup(Directory tempDir, String tempFileNamePrefix) {
this.tempDir = tempDir;
this.tempFileNamePrefix = tempFileNamePrefix;
}
// TODO: Review if this comparator is really needed for TST to work correctly!!!
/** TST uses UTF-16 sorting, so we need a suitable BytesRef comparator to do this. */
private final static Comparator<BytesRef> utf8SortedAsUTF16SortOrder = (a, b) -> {
final byte[] aBytes = a.bytes;
int aUpto = a.offset;
final byte[] bBytes = b.bytes;
int bUpto = b.offset;
final int aStop = aUpto + Math.min(a.length, b.length);
while(aUpto < aStop) {
int aByte = aBytes[aUpto++] & 0xff;
int bByte = bBytes[bUpto++] & 0xff;
if (aByte != bByte) {
// See http://icu-project.org/docs/papers/utf16_code_point_order.html#utf-8-in-utf-16-order
// We know the terms are not equal, but, we may
// have to carefully fixup the bytes at the
// difference to match UTF16's sort order:
// NOTE: instead of moving supplementary code points (0xee and 0xef) to the unused 0xfe and 0xff,
// we move them to the unused 0xfc and 0xfd [reserved for future 6-byte character sequences]
// this reserves 0xff for preflex's term reordering (surrogate dance), and if unicode grows such
// that 6-byte sequences are needed we have much bigger problems anyway.
if (aByte >= 0xee && bByte >= 0xee) {
if ((aByte & 0xfe) == 0xee) {
aByte += 0xe;
}
if ((bByte&0xfe) == 0xee) {
bByte += 0xe;
}
}
return aByte - bByte;
}
}
// One is a prefix of the other, or, they are equal:
return a.length - b.length;
};
@Override
public void build(InputIterator iterator) throws IOException {
if (iterator.hasPayloads()) {
throw new IllegalArgumentException("this suggester doesn't support payloads");
}
if (iterator.hasContexts()) {
throw new IllegalArgumentException("this suggester doesn't support contexts");
}
root = new TernaryTreeNode();
// make sure it's sorted and the comparator uses UTF16 sort order
iterator = new SortedInputIterator(tempDir, tempFileNamePrefix, iterator, utf8SortedAsUTF16SortOrder);
count = 0;
ArrayList<String> tokens = new ArrayList<>();
ArrayList<Number> vals = new ArrayList<>();
BytesRef spare;
CharsRefBuilder charsSpare = new CharsRefBuilder();
while ((spare = iterator.next()) != null) {
charsSpare.copyUTF8Bytes(spare);
tokens.add(charsSpare.toString());
vals.add(Long.valueOf(iterator.weight()));
count++;
}
autocomplete.balancedTree(tokens.toArray(), vals.toArray(), 0, tokens.size() - 1, root);
}
/**
* Adds a new node if <code>key</code> already exists,
* otherwise replaces its value.
* <p>
* This method always returns true.
*/
public boolean add(CharSequence key, Object value) {
autocomplete.insert(root, key, value, 0);
// XXX we don't know if a new node was created
return true;
}
/**
* Returns the value for the specified key, or null
* if the key does not exist.
*/
public Object get(CharSequence key) {
List<TernaryTreeNode> list = autocomplete.prefixCompletion(root, key, 0);
if (list == null || list.isEmpty()) {
return null;
}
for (TernaryTreeNode n : list) {
if (charSeqEquals(n.token, key)) {
return n.val;
}
}
return null;
}
private static boolean charSeqEquals(CharSequence left, CharSequence right) {
int len = left.length();
if (len != right.length()) {
return false;
}
for (int i = 0; i < len; i++) {
if (left.charAt(i) != right.charAt(i)) {
return false;
}
}
return true;
}
@Override
public List<LookupResult> lookup(CharSequence key, Set<BytesRef> contexts, boolean onlyMorePopular, int num) {
if (contexts != null) {
throw new IllegalArgumentException("this suggester doesn't support contexts");
}
List<TernaryTreeNode> list = autocomplete.prefixCompletion(root, key, 0);
List<LookupResult> res = new ArrayList<>();
if (list == null || list.size() == 0) {
return res;
}
int maxCnt = Math.min(num, list.size());
if (onlyMorePopular) {
LookupPriorityQueue queue = new LookupPriorityQueue(num);
for (TernaryTreeNode ttn : list) {
queue.insertWithOverflow(new LookupResult(ttn.token, ((Number)ttn.val).longValue()));
}
for (LookupResult lr : queue.getResults()) {
res.add(lr);
}
} else {
for (int i = 0; i < maxCnt; i++) {
TernaryTreeNode ttn = list.get(i);
res.add(new LookupResult(ttn.token, ((Number)ttn.val).longValue()));
}
}
return res;
}
private static final byte LO_KID = 0x01;
private static final byte EQ_KID = 0x02;
private static final byte HI_KID = 0x04;
private static final byte HAS_TOKEN = 0x08;
private static final byte HAS_VALUE = 0x10;
// pre-order traversal
private void readRecursively(DataInput in, TernaryTreeNode node) throws IOException {
node.splitchar = in.readString().charAt(0);
byte mask = in.readByte();
if ((mask & HAS_TOKEN) != 0) {
node.token = in.readString();
}
if ((mask & HAS_VALUE) != 0) {
node.val = Long.valueOf(in.readLong());
}
if ((mask & LO_KID) != 0) {
node.loKid = new TernaryTreeNode();
readRecursively(in, node.loKid);
}
if ((mask & EQ_KID) != 0) {
node.eqKid = new TernaryTreeNode();
readRecursively(in, node.eqKid);
}
if ((mask & HI_KID) != 0) {
node.hiKid = new TernaryTreeNode();
readRecursively(in, node.hiKid);
}
}
// pre-order traversal
private void writeRecursively(DataOutput out, TernaryTreeNode node) throws IOException {
// write out the current node
out.writeString(new String(new char[] {node.splitchar}, 0, 1));
// prepare a mask of kids
byte mask = 0;
if (node.eqKid != null) mask |= EQ_KID;
if (node.loKid != null) mask |= LO_KID;
if (node.hiKid != null) mask |= HI_KID;
if (node.token != null) mask |= HAS_TOKEN;
if (node.val != null) mask |= HAS_VALUE;
out.writeByte(mask);
if (node.token != null) out.writeString(node.token);
if (node.val != null) out.writeLong(((Number)node.val).longValue());
// recurse and write kids
if (node.loKid != null) {
writeRecursively(out, node.loKid);
}
if (node.eqKid != null) {
writeRecursively(out, node.eqKid);
}
if (node.hiKid != null) {
writeRecursively(out, node.hiKid);
}
}
@Override
public synchronized boolean store(DataOutput output) throws IOException {
output.writeVLong(count);
writeRecursively(output, root);
return true;
}
@Override
public synchronized boolean load(DataInput input) throws IOException {
count = input.readVLong();
root = new TernaryTreeNode();
readRecursively(input, root);
return true;
}
/** Returns byte size of the underlying TST */
@Override
public long ramBytesUsed() {
long mem = RamUsageEstimator.shallowSizeOf(this);
if (root != null) {
mem += root.sizeInBytes();
}
return mem;
}
@Override
public long getCount() {
return count;
}
}