mikemccand commented on code in PR #16128: URL: https://github.com/apache/lucene/pull/16128#discussion_r3587079202
########## lucene/core/src/java/org/apache/lucene/index/PrimaryKeyLookup.java: ########## @@ -0,0 +1,185 @@ +/* + * 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.index; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.lucene.index.IndexReader.CacheHelper; +import org.apache.lucene.util.Bits; +import org.apache.lucene.util.BytesRef; + +/** + * Class to do efficient primary-key lookups. Instances are not thread-safe. Each calling thread + * must own its own instance, and an instance must only ever be used from the thread that + * constructed it. When assertions are enabled this invariant is checked on every lookup and reopen + * call. Do not use this if a term may appear in more than one document! It will only return the + * first one it finds. + * + * @lucene.experimental + */ +public class PrimaryKeyLookup { + + /** Name of the primary-key field. */ + protected final String idFieldName; + + /** Per-segment {@link TermsEnum}. */ + protected final TermsEnum[] termsEnums; + + /** Per-segment {@link PostingsEnum}. */ + protected final PostingsEnum[] postingsEnums; + + /** Per-segment live-docs bitset. */ + protected final Bits[] liveDocs; + + /** Per-segment {@code docBase} used to translate leaf docIDs to absolute docIDs. */ + protected final int[] docBases; + + /** Number of populated entries in termsEnums/postingsEnums. */ + protected final int numEnums; + + /** True if any segment in this lookup has deletions. */ + protected final boolean hasDeletions; + + /** + * Maps a segment core cache key to its index in termsEnums/postingsEnums, so that reopen can + * transfer per-segment enum state to the next generation. + */ + protected final Map<IndexReader.CacheKey, Integer> enumIndexes; + + // Thread-stickiness guard. Review Comment: Maybe add "only checked under assert" or so? ########## lucene/core/src/java/org/apache/lucene/index/PrimaryKeyLookup.java: ########## @@ -0,0 +1,185 @@ +/* + * 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.index; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.lucene.index.IndexReader.CacheHelper; +import org.apache.lucene.util.Bits; +import org.apache.lucene.util.BytesRef; + +/** + * Class to do efficient primary-key lookups. Instances are not thread-safe. Each calling thread + * must own its own instance, and an instance must only ever be used from the thread that + * constructed it. When assertions are enabled this invariant is checked on every lookup and reopen + * call. Do not use this if a term may appear in more than one document! It will only return the + * first one it finds. + * + * @lucene.experimental + */ +public class PrimaryKeyLookup { + + /** Name of the primary-key field. */ + protected final String idFieldName; + + /** Per-segment {@link TermsEnum}. */ + protected final TermsEnum[] termsEnums; + + /** Per-segment {@link PostingsEnum}. */ + protected final PostingsEnum[] postingsEnums; + + /** Per-segment live-docs bitset. */ + protected final Bits[] liveDocs; + + /** Per-segment {@code docBase} used to translate leaf docIDs to absolute docIDs. */ + protected final int[] docBases; + + /** Number of populated entries in termsEnums/postingsEnums. */ + protected final int numEnums; + + /** True if any segment in this lookup has deletions. */ + protected final boolean hasDeletions; + + /** + * Maps a segment core cache key to its index in termsEnums/postingsEnums, so that reopen can + * transfer per-segment enum state to the next generation. + */ + protected final Map<IndexReader.CacheKey, Integer> enumIndexes; + + // Thread-stickiness guard. + private final long ownerThreadId; + + /** Construct a {@code PrimaryKeyLookup} bound to {@code reader}. */ + public PrimaryKeyLookup(IndexReader reader, String idFieldName) throws IOException { + this(reader, idFieldName, Collections.emptyMap(), null, null); + } + + /** Construct a {@code PrimaryKeyLookup} instance. */ Review Comment: Maybe also say "This use used for refreshing to a new reader" or so? ########## lucene/core/src/java/org/apache/lucene/index/PrimaryKeyLookup.java: ########## @@ -0,0 +1,185 @@ +/* + * 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.index; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.lucene.index.IndexReader.CacheHelper; +import org.apache.lucene.util.Bits; +import org.apache.lucene.util.BytesRef; + +/** + * Class to do efficient primary-key lookups. Instances are not thread-safe. Each calling thread + * must own its own instance, and an instance must only ever be used from the thread that + * constructed it. When assertions are enabled this invariant is checked on every lookup and reopen + * call. Do not use this if a term may appear in more than one document! It will only return the + * first one it finds. + * + * @lucene.experimental + */ +public class PrimaryKeyLookup { + + /** Name of the primary-key field. */ + protected final String idFieldName; + + /** Per-segment {@link TermsEnum}. */ + protected final TermsEnum[] termsEnums; + + /** Per-segment {@link PostingsEnum}. */ + protected final PostingsEnum[] postingsEnums; + + /** Per-segment live-docs bitset. */ + protected final Bits[] liveDocs; + + /** Per-segment {@code docBase} used to translate leaf docIDs to absolute docIDs. */ + protected final int[] docBases; + + /** Number of populated entries in termsEnums/postingsEnums. */ + protected final int numEnums; + + /** True if any segment in this lookup has deletions. */ + protected final boolean hasDeletions; + + /** + * Maps a segment core cache key to its index in termsEnums/postingsEnums, so that reopen can + * transfer per-segment enum state to the next generation. + */ + protected final Map<IndexReader.CacheKey, Integer> enumIndexes; + + // Thread-stickiness guard. + private final long ownerThreadId; + + /** Construct a {@code PrimaryKeyLookup} bound to {@code reader}. */ + public PrimaryKeyLookup(IndexReader reader, String idFieldName) throws IOException { + this(reader, idFieldName, Collections.emptyMap(), null, null); + } + + /** Construct a {@code PrimaryKeyLookup} instance. */ + protected PrimaryKeyLookup( + IndexReader reader, + String idFieldName, + Map<IndexReader.CacheKey, Integer> prevEnumIndexes, + TermsEnum[] reusableTermsEnums, + PostingsEnum[] reusablePostingsEnums) + throws IOException { + this.idFieldName = idFieldName; + this.ownerThreadId = Thread.currentThread().threadId(); + + List<LeafReaderContext> leaves = new ArrayList<>(reader.leaves()); + // Larger segments are more likely to have the id, so we sort largest to smallest by numDocs: + leaves.sort((c1, c2) -> c2.reader().numDocs() - c1.reader().numDocs()); + + termsEnums = new TermsEnum[leaves.size()]; + postingsEnums = new PostingsEnum[leaves.size()]; + liveDocs = new Bits[leaves.size()]; + docBases = new int[leaves.size()]; + enumIndexes = new HashMap<>(); + int numEnums = 0; + boolean hasDeletions = false; + + for (LeafReaderContext context : leaves) { + LeafReader leafReader = context.reader(); + CacheHelper cacheHelper = leafReader.getCoreCacheHelper(); + IndexReader.CacheKey cacheKey = cacheHelper == null ? null : cacheHelper.getKey(); + + if (cacheKey != null && prevEnumIndexes.containsKey(cacheKey)) { + // Reuse termsEnum, postingsEnum. + int seg = prevEnumIndexes.get(cacheKey); + termsEnums[numEnums] = reusableTermsEnums[seg]; + postingsEnums[numEnums] = reusablePostingsEnums[seg]; + } else { + // New or empty segment. + Terms terms = leafReader.terms(idFieldName); + if (terms != null) { + termsEnums[numEnums] = terms.iterator(); + assert termsEnums[numEnums] != null; + } + } + + if (termsEnums[numEnums] != null) { Review Comment: Maybe `numEnums` -> `numEnum`? CC @msokolov! ########## lucene/core/src/java/org/apache/lucene/index/PrimaryKeyLookup.java: ########## @@ -0,0 +1,185 @@ +/* + * 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.index; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.lucene.index.IndexReader.CacheHelper; +import org.apache.lucene.util.Bits; +import org.apache.lucene.util.BytesRef; + +/** + * Class to do efficient primary-key lookups. Instances are not thread-safe. Each calling thread + * must own its own instance, and an instance must only ever be used from the thread that + * constructed it. When assertions are enabled this invariant is checked on every lookup and reopen + * call. Do not use this if a term may appear in more than one document! It will only return the + * first one it finds. + * + * @lucene.experimental + */ +public class PrimaryKeyLookup { + + /** Name of the primary-key field. */ + protected final String idFieldName; + + /** Per-segment {@link TermsEnum}. */ + protected final TermsEnum[] termsEnums; + + /** Per-segment {@link PostingsEnum}. */ + protected final PostingsEnum[] postingsEnums; + + /** Per-segment live-docs bitset. */ + protected final Bits[] liveDocs; + + /** Per-segment {@code docBase} used to translate leaf docIDs to absolute docIDs. */ + protected final int[] docBases; + + /** Number of populated entries in termsEnums/postingsEnums. */ + protected final int numEnums; + + /** True if any segment in this lookup has deletions. */ + protected final boolean hasDeletions; + + /** + * Maps a segment core cache key to its index in termsEnums/postingsEnums, so that reopen can + * transfer per-segment enum state to the next generation. + */ + protected final Map<IndexReader.CacheKey, Integer> enumIndexes; + + // Thread-stickiness guard. + private final long ownerThreadId; + + /** Construct a {@code PrimaryKeyLookup} bound to {@code reader}. */ + public PrimaryKeyLookup(IndexReader reader, String idFieldName) throws IOException { + this(reader, idFieldName, Collections.emptyMap(), null, null); + } + + /** Construct a {@code PrimaryKeyLookup} instance. */ + protected PrimaryKeyLookup( + IndexReader reader, + String idFieldName, + Map<IndexReader.CacheKey, Integer> prevEnumIndexes, + TermsEnum[] reusableTermsEnums, + PostingsEnum[] reusablePostingsEnums) + throws IOException { + this.idFieldName = idFieldName; + this.ownerThreadId = Thread.currentThread().threadId(); + + List<LeafReaderContext> leaves = new ArrayList<>(reader.leaves()); + // Larger segments are more likely to have the id, so we sort largest to smallest by numDocs: + leaves.sort((c1, c2) -> c2.reader().numDocs() - c1.reader().numDocs()); + + termsEnums = new TermsEnum[leaves.size()]; + postingsEnums = new PostingsEnum[leaves.size()]; + liveDocs = new Bits[leaves.size()]; + docBases = new int[leaves.size()]; + enumIndexes = new HashMap<>(); + int numEnums = 0; + boolean hasDeletions = false; + + for (LeafReaderContext context : leaves) { + LeafReader leafReader = context.reader(); + CacheHelper cacheHelper = leafReader.getCoreCacheHelper(); + IndexReader.CacheKey cacheKey = cacheHelper == null ? null : cacheHelper.getKey(); + + if (cacheKey != null && prevEnumIndexes.containsKey(cacheKey)) { + // Reuse termsEnum, postingsEnum. + int seg = prevEnumIndexes.get(cacheKey); + termsEnums[numEnums] = reusableTermsEnums[seg]; + postingsEnums[numEnums] = reusablePostingsEnums[seg]; + } else { + // New or empty segment. + Terms terms = leafReader.terms(idFieldName); + if (terms != null) { + termsEnums[numEnums] = terms.iterator(); + assert termsEnums[numEnums] != null; + } + } + + if (termsEnums[numEnums] != null) { + if (cacheKey != null) { + enumIndexes.put(cacheKey, numEnums); + } + + docBases[numEnums] = context.docBase; + liveDocs[numEnums] = leafReader.getLiveDocs(); + hasDeletions |= leafReader.hasDeletions(); + + numEnums++; + } + } + + this.numEnums = numEnums; + this.hasDeletions = hasDeletions; + } + + /** Returns docID if found, else -1. */ + public int lookup(BytesRef id) throws IOException { + assert assertOwnerThread(); + for (int seg = 0; seg < numEnums; seg++) { Review Comment: Maybe `numEnum` -> `numSegment`? ########## lucene/core/src/java/org/apache/lucene/index/PrimaryKeyLookup.java: ########## @@ -0,0 +1,185 @@ +/* + * 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.index; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.lucene.index.IndexReader.CacheHelper; +import org.apache.lucene.util.Bits; +import org.apache.lucene.util.BytesRef; + +/** + * Class to do efficient primary-key lookups. Instances are not thread-safe. Each calling thread + * must own its own instance, and an instance must only ever be used from the thread that + * constructed it. When assertions are enabled this invariant is checked on every lookup and reopen + * call. Do not use this if a term may appear in more than one document! It will only return the + * first one it finds. + * + * @lucene.experimental + */ +public class PrimaryKeyLookup { + + /** Name of the primary-key field. */ + protected final String idFieldName; + + /** Per-segment {@link TermsEnum}. */ + protected final TermsEnum[] termsEnums; + + /** Per-segment {@link PostingsEnum}. */ + protected final PostingsEnum[] postingsEnums; + + /** Per-segment live-docs bitset. */ + protected final Bits[] liveDocs; + + /** Per-segment {@code docBase} used to translate leaf docIDs to absolute docIDs. */ + protected final int[] docBases; + + /** Number of populated entries in termsEnums/postingsEnums. */ + protected final int numEnums; + + /** True if any segment in this lookup has deletions. */ + protected final boolean hasDeletions; + + /** + * Maps a segment core cache key to its index in termsEnums/postingsEnums, so that reopen can + * transfer per-segment enum state to the next generation. + */ + protected final Map<IndexReader.CacheKey, Integer> enumIndexes; + + // Thread-stickiness guard. + private final long ownerThreadId; + + /** Construct a {@code PrimaryKeyLookup} bound to {@code reader}. */ + public PrimaryKeyLookup(IndexReader reader, String idFieldName) throws IOException { + this(reader, idFieldName, Collections.emptyMap(), null, null); + } + + /** Construct a {@code PrimaryKeyLookup} instance. */ + protected PrimaryKeyLookup( + IndexReader reader, + String idFieldName, + Map<IndexReader.CacheKey, Integer> prevEnumIndexes, + TermsEnum[] reusableTermsEnums, + PostingsEnum[] reusablePostingsEnums) + throws IOException { + this.idFieldName = idFieldName; + this.ownerThreadId = Thread.currentThread().threadId(); + + List<LeafReaderContext> leaves = new ArrayList<>(reader.leaves()); + // Larger segments are more likely to have the id, so we sort largest to smallest by numDocs: + leaves.sort((c1, c2) -> c2.reader().numDocs() - c1.reader().numDocs()); + + termsEnums = new TermsEnum[leaves.size()]; + postingsEnums = new PostingsEnum[leaves.size()]; + liveDocs = new Bits[leaves.size()]; + docBases = new int[leaves.size()]; + enumIndexes = new HashMap<>(); + int numEnums = 0; + boolean hasDeletions = false; + + for (LeafReaderContext context : leaves) { + LeafReader leafReader = context.reader(); + CacheHelper cacheHelper = leafReader.getCoreCacheHelper(); + IndexReader.CacheKey cacheKey = cacheHelper == null ? null : cacheHelper.getKey(); + + if (cacheKey != null && prevEnumIndexes.containsKey(cacheKey)) { + // Reuse termsEnum, postingsEnum. + int seg = prevEnumIndexes.get(cacheKey); + termsEnums[numEnums] = reusableTermsEnums[seg]; + postingsEnums[numEnums] = reusablePostingsEnums[seg]; + } else { + // New or empty segment. + Terms terms = leafReader.terms(idFieldName); + if (terms != null) { + termsEnums[numEnums] = terms.iterator(); + assert termsEnums[numEnums] != null; + } + } + + if (termsEnums[numEnums] != null) { + if (cacheKey != null) { + enumIndexes.put(cacheKey, numEnums); + } + + docBases[numEnums] = context.docBase; + liveDocs[numEnums] = leafReader.getLiveDocs(); + hasDeletions |= leafReader.hasDeletions(); + + numEnums++; + } + } + + this.numEnums = numEnums; + this.hasDeletions = hasDeletions; + } + + /** Returns docID if found, else -1. */ + public int lookup(BytesRef id) throws IOException { + assert assertOwnerThread(); + for (int seg = 0; seg < numEnums; seg++) { + if (termsEnums[seg].seekExact(id) == false) { + continue; + } + postingsEnums[seg] = termsEnums[seg].postings(postingsEnums[seg], 0); + + if (liveDocs[seg] == null) { + // Fast path: segment has no deletions. + int docID = postingsEnums[seg].nextDoc(); + assert docID != PostingsEnum.NO_MORE_DOCS; + return docBases[seg] + docID; + } + + // Slow path: segment has deletions; scan until we find a live doc or exhaust. + int docID; + while ((docID = postingsEnums[seg].nextDoc()) != PostingsEnum.NO_MORE_DOCS) { + if (liveDocs[seg].get(docID)) { + return docBases[seg] + docID; + } + } + + assert hasDeletions; + } + return -1; + } + + /** + * Returns a new PrimaryKeyLookup instance bound reusing this instance per-segment + * TermsEnum/PostingsEnum etc. + */ + public PrimaryKeyLookup reopen(IndexReader reader) throws IOException { + assert assertOwnerThread(); + if (reader == null) { + return null; + } + return new PrimaryKeyLookup( Review Comment: Is there ever a time when we'd `return this` because nothing changed? ########## lucene/core/src/java/org/apache/lucene/index/PrimaryKeyLookup.java: ########## @@ -0,0 +1,185 @@ +/* + * 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.index; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.lucene.index.IndexReader.CacheHelper; +import org.apache.lucene.util.Bits; +import org.apache.lucene.util.BytesRef; + +/** + * Class to do efficient primary-key lookups. Instances are not thread-safe. Each calling thread + * must own its own instance, and an instance must only ever be used from the thread that + * constructed it. When assertions are enabled this invariant is checked on every lookup and reopen + * call. Do not use this if a term may appear in more than one document! It will only return the + * first one it finds. + * + * @lucene.experimental + */ +public class PrimaryKeyLookup { + + /** Name of the primary-key field. */ + protected final String idFieldName; + + /** Per-segment {@link TermsEnum}. */ + protected final TermsEnum[] termsEnums; + + /** Per-segment {@link PostingsEnum}. */ + protected final PostingsEnum[] postingsEnums; + + /** Per-segment live-docs bitset. */ + protected final Bits[] liveDocs; + + /** Per-segment {@code docBase} used to translate leaf docIDs to absolute docIDs. */ + protected final int[] docBases; + + /** Number of populated entries in termsEnums/postingsEnums. */ + protected final int numEnums; + + /** True if any segment in this lookup has deletions. */ + protected final boolean hasDeletions; + + /** + * Maps a segment core cache key to its index in termsEnums/postingsEnums, so that reopen can + * transfer per-segment enum state to the next generation. + */ + protected final Map<IndexReader.CacheKey, Integer> enumIndexes; + + // Thread-stickiness guard. + private final long ownerThreadId; + + /** Construct a {@code PrimaryKeyLookup} bound to {@code reader}. */ + public PrimaryKeyLookup(IndexReader reader, String idFieldName) throws IOException { + this(reader, idFieldName, Collections.emptyMap(), null, null); + } + + /** Construct a {@code PrimaryKeyLookup} instance. */ + protected PrimaryKeyLookup( + IndexReader reader, + String idFieldName, + Map<IndexReader.CacheKey, Integer> prevEnumIndexes, + TermsEnum[] reusableTermsEnums, + PostingsEnum[] reusablePostingsEnums) + throws IOException { + this.idFieldName = idFieldName; + this.ownerThreadId = Thread.currentThread().threadId(); + + List<LeafReaderContext> leaves = new ArrayList<>(reader.leaves()); + // Larger segments are more likely to have the id, so we sort largest to smallest by numDocs: + leaves.sort((c1, c2) -> c2.reader().numDocs() - c1.reader().numDocs()); + + termsEnums = new TermsEnum[leaves.size()]; + postingsEnums = new PostingsEnum[leaves.size()]; + liveDocs = new Bits[leaves.size()]; + docBases = new int[leaves.size()]; + enumIndexes = new HashMap<>(); + int numEnums = 0; + boolean hasDeletions = false; + + for (LeafReaderContext context : leaves) { + LeafReader leafReader = context.reader(); + CacheHelper cacheHelper = leafReader.getCoreCacheHelper(); + IndexReader.CacheKey cacheKey = cacheHelper == null ? null : cacheHelper.getKey(); + + if (cacheKey != null && prevEnumIndexes.containsKey(cacheKey)) { + // Reuse termsEnum, postingsEnum. + int seg = prevEnumIndexes.get(cacheKey); + termsEnums[numEnums] = reusableTermsEnums[seg]; + postingsEnums[numEnums] = reusablePostingsEnums[seg]; + } else { + // New or empty segment. + Terms terms = leafReader.terms(idFieldName); + if (terms != null) { + termsEnums[numEnums] = terms.iterator(); + assert termsEnums[numEnums] != null; + } + } + + if (termsEnums[numEnums] != null) { + if (cacheKey != null) { + enumIndexes.put(cacheKey, numEnums); + } + + docBases[numEnums] = context.docBase; + liveDocs[numEnums] = leafReader.getLiveDocs(); + hasDeletions |= leafReader.hasDeletions(); + + numEnums++; + } + } + + this.numEnums = numEnums; + this.hasDeletions = hasDeletions; + } + + /** Returns docID if found, else -1. */ + public int lookup(BytesRef id) throws IOException { + assert assertOwnerThread(); + for (int seg = 0; seg < numEnums; seg++) { + if (termsEnums[seg].seekExact(id) == false) { + continue; + } + postingsEnums[seg] = termsEnums[seg].postings(postingsEnums[seg], 0); + + if (liveDocs[seg] == null) { + // Fast path: segment has no deletions. + int docID = postingsEnums[seg].nextDoc(); + assert docID != PostingsEnum.NO_MORE_DOCS; + return docBases[seg] + docID; + } + + // Slow path: segment has deletions; scan until we find a live doc or exhaust. + int docID; + while ((docID = postingsEnums[seg].nextDoc()) != PostingsEnum.NO_MORE_DOCS) { + if (liveDocs[seg].get(docID)) { + return docBases[seg] + docID; + } + } + + assert hasDeletions; + } + return -1; + } + + /** + * Returns a new PrimaryKeyLookup instance bound reusing this instance per-segment + * TermsEnum/PostingsEnum etc. + */ + public PrimaryKeyLookup reopen(IndexReader reader) throws IOException { + assert assertOwnerThread(); + if (reader == null) { + return null; + } + return new PrimaryKeyLookup( + reader, this.idFieldName, this.enumIndexes, this.termsEnums, this.postingsEnums); + } + + private boolean assertOwnerThread() { + long current = Thread.currentThread().threadId(); + assert current == ownerThreadId + : "PrimaryKeyLookup is not thread-safe: it was created by thread " Review Comment: Maybe strengthen/restrict `not thread-safe` to `must be thread-private`? If it was simply not thread safe, caller could to their own locking to ensure only one (of many) threads called this instance at a time, but we also don't allow that. ########## lucene/core/src/java/org/apache/lucene/index/PrimaryKeyLookup.java: ########## @@ -0,0 +1,185 @@ +/* + * 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.index; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.lucene.index.IndexReader.CacheHelper; +import org.apache.lucene.util.Bits; +import org.apache.lucene.util.BytesRef; + +/** + * Class to do efficient primary-key lookups. Instances are not thread-safe. Each calling thread + * must own its own instance, and an instance must only ever be used from the thread that + * constructed it. When assertions are enabled this invariant is checked on every lookup and reopen + * call. Do not use this if a term may appear in more than one document! It will only return the + * first one it finds. + * + * @lucene.experimental + */ +public class PrimaryKeyLookup { + + /** Name of the primary-key field. */ + protected final String idFieldName; + + /** Per-segment {@link TermsEnum}. */ + protected final TermsEnum[] termsEnums; + + /** Per-segment {@link PostingsEnum}. */ + protected final PostingsEnum[] postingsEnums; + + /** Per-segment live-docs bitset. */ + protected final Bits[] liveDocs; + + /** Per-segment {@code docBase} used to translate leaf docIDs to absolute docIDs. */ + protected final int[] docBases; + + /** Number of populated entries in termsEnums/postingsEnums. */ + protected final int numEnums; + + /** True if any segment in this lookup has deletions. */ + protected final boolean hasDeletions; + + /** + * Maps a segment core cache key to its index in termsEnums/postingsEnums, so that reopen can + * transfer per-segment enum state to the next generation. + */ + protected final Map<IndexReader.CacheKey, Integer> enumIndexes; + + // Thread-stickiness guard. + private final long ownerThreadId; + + /** Construct a {@code PrimaryKeyLookup} bound to {@code reader}. */ + public PrimaryKeyLookup(IndexReader reader, String idFieldName) throws IOException { + this(reader, idFieldName, Collections.emptyMap(), null, null); + } + + /** Construct a {@code PrimaryKeyLookup} instance. */ + protected PrimaryKeyLookup( + IndexReader reader, + String idFieldName, + Map<IndexReader.CacheKey, Integer> prevEnumIndexes, + TermsEnum[] reusableTermsEnums, + PostingsEnum[] reusablePostingsEnums) + throws IOException { + this.idFieldName = idFieldName; + this.ownerThreadId = Thread.currentThread().threadId(); + + List<LeafReaderContext> leaves = new ArrayList<>(reader.leaves()); + // Larger segments are more likely to have the id, so we sort largest to smallest by numDocs: + leaves.sort((c1, c2) -> c2.reader().numDocs() - c1.reader().numDocs()); + + termsEnums = new TermsEnum[leaves.size()]; + postingsEnums = new PostingsEnum[leaves.size()]; + liveDocs = new Bits[leaves.size()]; + docBases = new int[leaves.size()]; + enumIndexes = new HashMap<>(); + int numEnums = 0; + boolean hasDeletions = false; + + for (LeafReaderContext context : leaves) { + LeafReader leafReader = context.reader(); + CacheHelper cacheHelper = leafReader.getCoreCacheHelper(); + IndexReader.CacheKey cacheKey = cacheHelper == null ? null : cacheHelper.getKey(); + + if (cacheKey != null && prevEnumIndexes.containsKey(cacheKey)) { + // Reuse termsEnum, postingsEnum. + int seg = prevEnumIndexes.get(cacheKey); + termsEnums[numEnums] = reusableTermsEnums[seg]; + postingsEnums[numEnums] = reusablePostingsEnums[seg]; + } else { + // New or empty segment. + Terms terms = leafReader.terms(idFieldName); + if (terms != null) { + termsEnums[numEnums] = terms.iterator(); + assert termsEnums[numEnums] != null; + } + } + + if (termsEnums[numEnums] != null) { + if (cacheKey != null) { + enumIndexes.put(cacheKey, numEnums); + } + + docBases[numEnums] = context.docBase; + liveDocs[numEnums] = leafReader.getLiveDocs(); + hasDeletions |= leafReader.hasDeletions(); + + numEnums++; + } + } + + this.numEnums = numEnums; + this.hasDeletions = hasDeletions; + } + + /** Returns docID if found, else -1. */ + public int lookup(BytesRef id) throws IOException { + assert assertOwnerThread(); + for (int seg = 0; seg < numEnums; seg++) { + if (termsEnums[seg].seekExact(id) == false) { Review Comment: Thank you for not using the dreaded `!`! ########## lucene/core/src/java/org/apache/lucene/index/PrimaryKeyLookup.java: ########## @@ -0,0 +1,185 @@ +/* + * 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.index; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.lucene.index.IndexReader.CacheHelper; +import org.apache.lucene.util.Bits; +import org.apache.lucene.util.BytesRef; + +/** + * Class to do efficient primary-key lookups. Instances are not thread-safe. Each calling thread + * must own its own instance, and an instance must only ever be used from the thread that + * constructed it. When assertions are enabled this invariant is checked on every lookup and reopen + * call. Do not use this if a term may appear in more than one document! It will only return the + * first one it finds. + * + * @lucene.experimental + */ +public class PrimaryKeyLookup { + + /** Name of the primary-key field. */ + protected final String idFieldName; + + /** Per-segment {@link TermsEnum}. */ + protected final TermsEnum[] termsEnums; + + /** Per-segment {@link PostingsEnum}. */ + protected final PostingsEnum[] postingsEnums; + + /** Per-segment live-docs bitset. */ + protected final Bits[] liveDocs; + + /** Per-segment {@code docBase} used to translate leaf docIDs to absolute docIDs. */ + protected final int[] docBases; + + /** Number of populated entries in termsEnums/postingsEnums. */ + protected final int numEnums; + + /** True if any segment in this lookup has deletions. */ + protected final boolean hasDeletions; + + /** + * Maps a segment core cache key to its index in termsEnums/postingsEnums, so that reopen can + * transfer per-segment enum state to the next generation. + */ + protected final Map<IndexReader.CacheKey, Integer> enumIndexes; + + // Thread-stickiness guard. + private final long ownerThreadId; + + /** Construct a {@code PrimaryKeyLookup} bound to {@code reader}. */ + public PrimaryKeyLookup(IndexReader reader, String idFieldName) throws IOException { + this(reader, idFieldName, Collections.emptyMap(), null, null); + } + + /** Construct a {@code PrimaryKeyLookup} instance. */ + protected PrimaryKeyLookup( + IndexReader reader, + String idFieldName, + Map<IndexReader.CacheKey, Integer> prevEnumIndexes, + TermsEnum[] reusableTermsEnums, + PostingsEnum[] reusablePostingsEnums) + throws IOException { + this.idFieldName = idFieldName; + this.ownerThreadId = Thread.currentThread().threadId(); + + List<LeafReaderContext> leaves = new ArrayList<>(reader.leaves()); + // Larger segments are more likely to have the id, so we sort largest to smallest by numDocs: + leaves.sort((c1, c2) -> c2.reader().numDocs() - c1.reader().numDocs()); + + termsEnums = new TermsEnum[leaves.size()]; + postingsEnums = new PostingsEnum[leaves.size()]; + liveDocs = new Bits[leaves.size()]; + docBases = new int[leaves.size()]; + enumIndexes = new HashMap<>(); + int numEnums = 0; + boolean hasDeletions = false; + + for (LeafReaderContext context : leaves) { + LeafReader leafReader = context.reader(); + CacheHelper cacheHelper = leafReader.getCoreCacheHelper(); + IndexReader.CacheKey cacheKey = cacheHelper == null ? null : cacheHelper.getKey(); + + if (cacheKey != null && prevEnumIndexes.containsKey(cacheKey)) { + // Reuse termsEnum, postingsEnum. + int seg = prevEnumIndexes.get(cacheKey); + termsEnums[numEnums] = reusableTermsEnums[seg]; + postingsEnums[numEnums] = reusablePostingsEnums[seg]; + } else { + // New or empty segment. + Terms terms = leafReader.terms(idFieldName); + if (terms != null) { Review Comment: Maybe add comment that this means no documents in this leaf had the field? ########## lucene/core/src/java/org/apache/lucene/index/PrimaryKeyLookup.java: ########## @@ -0,0 +1,185 @@ +/* + * 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.index; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.lucene.index.IndexReader.CacheHelper; +import org.apache.lucene.util.Bits; +import org.apache.lucene.util.BytesRef; + +/** + * Class to do efficient primary-key lookups. Instances are not thread-safe. Each calling thread Review Comment: Maybe spell out what a primary-key means here? "A field is a primary key if each of its terms uniquely identifies just one document in the index" or so? I think we have no requirement that every document has this field? E.g. in a multi-tenant case (indexing pencils and gummy-bears in a single index), one could use this class only for the pencils? So maybe say "Usually, every document will have a value for this field (its identifier) but this is not strictly required." or so? ########## lucene/core/src/java/org/apache/lucene/index/PrimaryKeyLookup.java: ########## @@ -0,0 +1,185 @@ +/* + * 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.index; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.lucene.index.IndexReader.CacheHelper; +import org.apache.lucene.util.Bits; +import org.apache.lucene.util.BytesRef; + +/** + * Class to do efficient primary-key lookups. Instances are not thread-safe. Each calling thread + * must own its own instance, and an instance must only ever be used from the thread that + * constructed it. When assertions are enabled this invariant is checked on every lookup and reopen + * call. Do not use this if a term may appear in more than one document! It will only return the + * first one it finds. + * + * @lucene.experimental + */ +public class PrimaryKeyLookup { + + /** Name of the primary-key field. */ + protected final String idFieldName; + + /** Per-segment {@link TermsEnum}. */ + protected final TermsEnum[] termsEnums; + + /** Per-segment {@link PostingsEnum}. */ + protected final PostingsEnum[] postingsEnums; + + /** Per-segment live-docs bitset. */ + protected final Bits[] liveDocs; + + /** Per-segment {@code docBase} used to translate leaf docIDs to absolute docIDs. */ + protected final int[] docBases; + + /** Number of populated entries in termsEnums/postingsEnums. */ + protected final int numEnums; + + /** True if any segment in this lookup has deletions. */ + protected final boolean hasDeletions; + + /** + * Maps a segment core cache key to its index in termsEnums/postingsEnums, so that reopen can + * transfer per-segment enum state to the next generation. + */ + protected final Map<IndexReader.CacheKey, Integer> enumIndexes; + + // Thread-stickiness guard. + private final long ownerThreadId; + + /** Construct a {@code PrimaryKeyLookup} bound to {@code reader}. */ + public PrimaryKeyLookup(IndexReader reader, String idFieldName) throws IOException { + this(reader, idFieldName, Collections.emptyMap(), null, null); + } + + /** Construct a {@code PrimaryKeyLookup} instance. */ + protected PrimaryKeyLookup( + IndexReader reader, + String idFieldName, + Map<IndexReader.CacheKey, Integer> prevEnumIndexes, + TermsEnum[] reusableTermsEnums, + PostingsEnum[] reusablePostingsEnums) + throws IOException { + this.idFieldName = idFieldName; + this.ownerThreadId = Thread.currentThread().threadId(); + + List<LeafReaderContext> leaves = new ArrayList<>(reader.leaves()); + // Larger segments are more likely to have the id, so we sort largest to smallest by numDocs: + leaves.sort((c1, c2) -> c2.reader().numDocs() - c1.reader().numDocs()); + + termsEnums = new TermsEnum[leaves.size()]; + postingsEnums = new PostingsEnum[leaves.size()]; + liveDocs = new Bits[leaves.size()]; + docBases = new int[leaves.size()]; + enumIndexes = new HashMap<>(); + int numEnums = 0; + boolean hasDeletions = false; + + for (LeafReaderContext context : leaves) { + LeafReader leafReader = context.reader(); + CacheHelper cacheHelper = leafReader.getCoreCacheHelper(); + IndexReader.CacheKey cacheKey = cacheHelper == null ? null : cacheHelper.getKey(); + + if (cacheKey != null && prevEnumIndexes.containsKey(cacheKey)) { + // Reuse termsEnum, postingsEnum. + int seg = prevEnumIndexes.get(cacheKey); + termsEnums[numEnums] = reusableTermsEnums[seg]; + postingsEnums[numEnums] = reusablePostingsEnums[seg]; + } else { + // New or empty segment. + Terms terms = leafReader.terms(idFieldName); + if (terms != null) { + termsEnums[numEnums] = terms.iterator(); + assert termsEnums[numEnums] != null; + } + } + + if (termsEnums[numEnums] != null) { + if (cacheKey != null) { + enumIndexes.put(cacheKey, numEnums); + } + + docBases[numEnums] = context.docBase; + liveDocs[numEnums] = leafReader.getLiveDocs(); + hasDeletions |= leafReader.hasDeletions(); + + numEnums++; + } + } + + this.numEnums = numEnums; + this.hasDeletions = hasDeletions; + } + + /** Returns docID if found, else -1. */ + public int lookup(BytesRef id) throws IOException { + assert assertOwnerThread(); + for (int seg = 0; seg < numEnums; seg++) { + if (termsEnums[seg].seekExact(id) == false) { + continue; + } + postingsEnums[seg] = termsEnums[seg].postings(postingsEnums[seg], 0); + + if (liveDocs[seg] == null) { + // Fast path: segment has no deletions. + int docID = postingsEnums[seg].nextDoc(); + assert docID != PostingsEnum.NO_MORE_DOCS; + return docBases[seg] + docID; + } + + // Slow path: segment has deletions; scan until we find a live doc or exhaust. + int docID; + while ((docID = postingsEnums[seg].nextDoc()) != PostingsEnum.NO_MORE_DOCS) { + if (liveDocs[seg].get(docID)) { + return docBases[seg] + docID; + } + } + + assert hasDeletions; + } + return -1; + } + + /** + * Returns a new PrimaryKeyLookup instance bound reusing this instance per-segment + * TermsEnum/PostingsEnum etc. Review Comment: Also state that the same thread ownership rules apply? ########## lucene/core/src/java/org/apache/lucene/index/PrimaryKeyLookup.java: ########## @@ -0,0 +1,185 @@ +/* + * 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.index; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.lucene.index.IndexReader.CacheHelper; +import org.apache.lucene.util.Bits; +import org.apache.lucene.util.BytesRef; + +/** + * Class to do efficient primary-key lookups. Instances are not thread-safe. Each calling thread + * must own its own instance, and an instance must only ever be used from the thread that + * constructed it. When assertions are enabled this invariant is checked on every lookup and reopen + * call. Do not use this if a term may appear in more than one document! It will only return the + * first one it finds. + * + * @lucene.experimental + */ +public class PrimaryKeyLookup { + + /** Name of the primary-key field. */ + protected final String idFieldName; + + /** Per-segment {@link TermsEnum}. */ + protected final TermsEnum[] termsEnums; + + /** Per-segment {@link PostingsEnum}. */ + protected final PostingsEnum[] postingsEnums; + + /** Per-segment live-docs bitset. */ + protected final Bits[] liveDocs; + + /** Per-segment {@code docBase} used to translate leaf docIDs to absolute docIDs. */ + protected final int[] docBases; + + /** Number of populated entries in termsEnums/postingsEnums. */ + protected final int numEnums; + + /** True if any segment in this lookup has deletions. */ + protected final boolean hasDeletions; + + /** + * Maps a segment core cache key to its index in termsEnums/postingsEnums, so that reopen can + * transfer per-segment enum state to the next generation. + */ + protected final Map<IndexReader.CacheKey, Integer> enumIndexes; + + // Thread-stickiness guard. + private final long ownerThreadId; + + /** Construct a {@code PrimaryKeyLookup} bound to {@code reader}. */ + public PrimaryKeyLookup(IndexReader reader, String idFieldName) throws IOException { + this(reader, idFieldName, Collections.emptyMap(), null, null); + } + + /** Construct a {@code PrimaryKeyLookup} instance. */ + protected PrimaryKeyLookup( + IndexReader reader, + String idFieldName, + Map<IndexReader.CacheKey, Integer> prevEnumIndexes, + TermsEnum[] reusableTermsEnums, + PostingsEnum[] reusablePostingsEnums) + throws IOException { + this.idFieldName = idFieldName; + this.ownerThreadId = Thread.currentThread().threadId(); + + List<LeafReaderContext> leaves = new ArrayList<>(reader.leaves()); + // Larger segments are more likely to have the id, so we sort largest to smallest by numDocs: + leaves.sort((c1, c2) -> c2.reader().numDocs() - c1.reader().numDocs()); + + termsEnums = new TermsEnum[leaves.size()]; + postingsEnums = new PostingsEnum[leaves.size()]; + liveDocs = new Bits[leaves.size()]; + docBases = new int[leaves.size()]; + enumIndexes = new HashMap<>(); + int numEnums = 0; + boolean hasDeletions = false; + + for (LeafReaderContext context : leaves) { + LeafReader leafReader = context.reader(); + CacheHelper cacheHelper = leafReader.getCoreCacheHelper(); + IndexReader.CacheKey cacheKey = cacheHelper == null ? null : cacheHelper.getKey(); + + if (cacheKey != null && prevEnumIndexes.containsKey(cacheKey)) { + // Reuse termsEnum, postingsEnum. + int seg = prevEnumIndexes.get(cacheKey); + termsEnums[numEnums] = reusableTermsEnums[seg]; + postingsEnums[numEnums] = reusablePostingsEnums[seg]; + } else { + // New or empty segment. + Terms terms = leafReader.terms(idFieldName); + if (terms != null) { + termsEnums[numEnums] = terms.iterator(); + assert termsEnums[numEnums] != null; + } + } + + if (termsEnums[numEnums] != null) { + if (cacheKey != null) { + enumIndexes.put(cacheKey, numEnums); + } + + docBases[numEnums] = context.docBase; + liveDocs[numEnums] = leafReader.getLiveDocs(); + hasDeletions |= leafReader.hasDeletions(); + + numEnums++; + } + } + + this.numEnums = numEnums; + this.hasDeletions = hasDeletions; + } + + /** Returns docID if found, else -1. */ + public int lookup(BytesRef id) throws IOException { + assert assertOwnerThread(); + for (int seg = 0; seg < numEnums; seg++) { + if (termsEnums[seg].seekExact(id) == false) { + continue; + } + postingsEnums[seg] = termsEnums[seg].postings(postingsEnums[seg], 0); + + if (liveDocs[seg] == null) { + // Fast path: segment has no deletions. + int docID = postingsEnums[seg].nextDoc(); + assert docID != PostingsEnum.NO_MORE_DOCS; + return docBases[seg] + docID; + } + + // Slow path: segment has deletions; scan until we find a live doc or exhaust. + int docID; + while ((docID = postingsEnums[seg].nextDoc()) != PostingsEnum.NO_MORE_DOCS) { + if (liveDocs[seg].get(docID)) { + return docBases[seg] + docID; + } + } + + assert hasDeletions; Review Comment: Maybe add comment like `The term did occur in this segment but those docs were all deleted; keep trying further segments" or so? ########## lucene/core/src/java/org/apache/lucene/index/PrimaryKeyLookup.java: ########## @@ -0,0 +1,185 @@ +/* + * 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.index; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.lucene.index.IndexReader.CacheHelper; +import org.apache.lucene.util.Bits; +import org.apache.lucene.util.BytesRef; + +/** + * Class to do efficient primary-key lookups. Instances are not thread-safe. Each calling thread + * must own its own instance, and an instance must only ever be used from the thread that + * constructed it. When assertions are enabled this invariant is checked on every lookup and reopen + * call. Do not use this if a term may appear in more than one document! It will only return the + * first one it finds. + * + * @lucene.experimental + */ +public class PrimaryKeyLookup { + + /** Name of the primary-key field. */ + protected final String idFieldName; + + /** Per-segment {@link TermsEnum}. */ + protected final TermsEnum[] termsEnums; + + /** Per-segment {@link PostingsEnum}. */ + protected final PostingsEnum[] postingsEnums; + + /** Per-segment live-docs bitset. */ + protected final Bits[] liveDocs; + + /** Per-segment {@code docBase} used to translate leaf docIDs to absolute docIDs. */ + protected final int[] docBases; + + /** Number of populated entries in termsEnums/postingsEnums. */ + protected final int numEnums; + + /** True if any segment in this lookup has deletions. */ + protected final boolean hasDeletions; + + /** + * Maps a segment core cache key to its index in termsEnums/postingsEnums, so that reopen can + * transfer per-segment enum state to the next generation. + */ + protected final Map<IndexReader.CacheKey, Integer> enumIndexes; + + // Thread-stickiness guard. + private final long ownerThreadId; + + /** Construct a {@code PrimaryKeyLookup} bound to {@code reader}. */ + public PrimaryKeyLookup(IndexReader reader, String idFieldName) throws IOException { + this(reader, idFieldName, Collections.emptyMap(), null, null); + } + + /** Construct a {@code PrimaryKeyLookup} instance. */ + protected PrimaryKeyLookup( + IndexReader reader, + String idFieldName, + Map<IndexReader.CacheKey, Integer> prevEnumIndexes, + TermsEnum[] reusableTermsEnums, + PostingsEnum[] reusablePostingsEnums) + throws IOException { + this.idFieldName = idFieldName; + this.ownerThreadId = Thread.currentThread().threadId(); + + List<LeafReaderContext> leaves = new ArrayList<>(reader.leaves()); + // Larger segments are more likely to have the id, so we sort largest to smallest by numDocs: + leaves.sort((c1, c2) -> c2.reader().numDocs() - c1.reader().numDocs()); + + termsEnums = new TermsEnum[leaves.size()]; + postingsEnums = new PostingsEnum[leaves.size()]; + liveDocs = new Bits[leaves.size()]; + docBases = new int[leaves.size()]; + enumIndexes = new HashMap<>(); + int numEnums = 0; + boolean hasDeletions = false; + + for (LeafReaderContext context : leaves) { + LeafReader leafReader = context.reader(); + CacheHelper cacheHelper = leafReader.getCoreCacheHelper(); + IndexReader.CacheKey cacheKey = cacheHelper == null ? null : cacheHelper.getKey(); + + if (cacheKey != null && prevEnumIndexes.containsKey(cacheKey)) { + // Reuse termsEnum, postingsEnum. + int seg = prevEnumIndexes.get(cacheKey); + termsEnums[numEnums] = reusableTermsEnums[seg]; + postingsEnums[numEnums] = reusablePostingsEnums[seg]; + } else { + // New or empty segment. + Terms terms = leafReader.terms(idFieldName); + if (terms != null) { + termsEnums[numEnums] = terms.iterator(); + assert termsEnums[numEnums] != null; + } + } + + if (termsEnums[numEnums] != null) { + if (cacheKey != null) { + enumIndexes.put(cacheKey, numEnums); + } + + docBases[numEnums] = context.docBase; + liveDocs[numEnums] = leafReader.getLiveDocs(); + hasDeletions |= leafReader.hasDeletions(); + + numEnums++; + } + } + + this.numEnums = numEnums; + this.hasDeletions = hasDeletions; + } + + /** Returns docID if found, else -1. */ + public int lookup(BytesRef id) throws IOException { + assert assertOwnerThread(); + for (int seg = 0; seg < numEnums; seg++) { + if (termsEnums[seg].seekExact(id) == false) { + continue; + } + postingsEnums[seg] = termsEnums[seg].postings(postingsEnums[seg], 0); + + if (liveDocs[seg] == null) { + // Fast path: segment has no deletions. + int docID = postingsEnums[seg].nextDoc(); + assert docID != PostingsEnum.NO_MORE_DOCS; + return docBases[seg] + docID; + } + + // Slow path: segment has deletions; scan until we find a live doc or exhaust. + int docID; + while ((docID = postingsEnums[seg].nextDoc()) != PostingsEnum.NO_MORE_DOCS) { + if (liveDocs[seg].get(docID)) { + return docBases[seg] + docID; + } + } + + assert hasDeletions; + } + return -1; + } + + /** + * Returns a new PrimaryKeyLookup instance bound reusing this instance per-segment + * TermsEnum/PostingsEnum etc. + */ + public PrimaryKeyLookup reopen(IndexReader reader) throws IOException { + assert assertOwnerThread(); + if (reader == null) { Review Comment: Hmm should we throw `NPE`? Hmm or should we `return this` -- `reopenIfChanged` returns `null` if there are no changes (not sure)? ########## lucene/core/src/java/org/apache/lucene/index/PrimaryKeyLookup.java: ########## @@ -0,0 +1,185 @@ +/* + * 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.index; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.lucene.index.IndexReader.CacheHelper; +import org.apache.lucene.util.Bits; +import org.apache.lucene.util.BytesRef; + +/** + * Class to do efficient primary-key lookups. Instances are not thread-safe. Each calling thread + * must own its own instance, and an instance must only ever be used from the thread that + * constructed it. When assertions are enabled this invariant is checked on every lookup and reopen + * call. Do not use this if a term may appear in more than one document! It will only return the + * first one it finds. + * + * @lucene.experimental + */ +public class PrimaryKeyLookup { + + /** Name of the primary-key field. */ + protected final String idFieldName; + + /** Per-segment {@link TermsEnum}. */ + protected final TermsEnum[] termsEnums; + + /** Per-segment {@link PostingsEnum}. */ + protected final PostingsEnum[] postingsEnums; + + /** Per-segment live-docs bitset. */ + protected final Bits[] liveDocs; + + /** Per-segment {@code docBase} used to translate leaf docIDs to absolute docIDs. */ + protected final int[] docBases; + + /** Number of populated entries in termsEnums/postingsEnums. */ + protected final int numEnums; + + /** True if any segment in this lookup has deletions. */ + protected final boolean hasDeletions; + + /** + * Maps a segment core cache key to its index in termsEnums/postingsEnums, so that reopen can + * transfer per-segment enum state to the next generation. + */ + protected final Map<IndexReader.CacheKey, Integer> enumIndexes; + + // Thread-stickiness guard. + private final long ownerThreadId; + + /** Construct a {@code PrimaryKeyLookup} bound to {@code reader}. */ + public PrimaryKeyLookup(IndexReader reader, String idFieldName) throws IOException { + this(reader, idFieldName, Collections.emptyMap(), null, null); + } + + /** Construct a {@code PrimaryKeyLookup} instance. */ + protected PrimaryKeyLookup( + IndexReader reader, + String idFieldName, + Map<IndexReader.CacheKey, Integer> prevEnumIndexes, + TermsEnum[] reusableTermsEnums, + PostingsEnum[] reusablePostingsEnums) + throws IOException { + this.idFieldName = idFieldName; + this.ownerThreadId = Thread.currentThread().threadId(); + + List<LeafReaderContext> leaves = new ArrayList<>(reader.leaves()); + // Larger segments are more likely to have the id, so we sort largest to smallest by numDocs: + leaves.sort((c1, c2) -> c2.reader().numDocs() - c1.reader().numDocs()); + + termsEnums = new TermsEnum[leaves.size()]; + postingsEnums = new PostingsEnum[leaves.size()]; + liveDocs = new Bits[leaves.size()]; + docBases = new int[leaves.size()]; + enumIndexes = new HashMap<>(); + int numEnums = 0; + boolean hasDeletions = false; + + for (LeafReaderContext context : leaves) { + LeafReader leafReader = context.reader(); + CacheHelper cacheHelper = leafReader.getCoreCacheHelper(); + IndexReader.CacheKey cacheKey = cacheHelper == null ? null : cacheHelper.getKey(); + + if (cacheKey != null && prevEnumIndexes.containsKey(cacheKey)) { + // Reuse termsEnum, postingsEnum. + int seg = prevEnumIndexes.get(cacheKey); + termsEnums[numEnums] = reusableTermsEnums[seg]; + postingsEnums[numEnums] = reusablePostingsEnums[seg]; + } else { + // New or empty segment. + Terms terms = leafReader.terms(idFieldName); + if (terms != null) { + termsEnums[numEnums] = terms.iterator(); + assert termsEnums[numEnums] != null; + } + } + + if (termsEnums[numEnums] != null) { + if (cacheKey != null) { + enumIndexes.put(cacheKey, numEnums); + } + + docBases[numEnums] = context.docBase; + liveDocs[numEnums] = leafReader.getLiveDocs(); + hasDeletions |= leafReader.hasDeletions(); + + numEnums++; + } + } + + this.numEnums = numEnums; Review Comment: Do we have test coverage for when there are no leaves to check (`numEnums == 0`)? ########## lucene/core/src/test/org/apache/lucene/index/TestPrimaryKeyLookup.java: ########## @@ -0,0 +1,196 @@ +/* + * 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.index; + +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.KeywordField; +import org.apache.lucene.store.Directory; +import org.apache.lucene.tests.analysis.MockAnalyzer; +import org.apache.lucene.tests.util.LuceneTestCase; + +public class TestPrimaryKeyLookup extends LuceneTestCase { + + public void testReopen() throws Exception { + Directory dir = newDirectory(); + IndexWriter writer = + new IndexWriter( + dir, + new IndexWriterConfig(new MockAnalyzer(random())) + .setMergePolicy(NoMergePolicy.INSTANCE)); + + Document doc; + doc = new Document(); + doc.add(new KeywordField("PK", "1", Field.Store.NO)); + writer.addDocument(doc); + + doc = new Document(); + doc.add(new KeywordField("PK", "2", Field.Store.NO)); + writer.addDocument(doc); + writer.flush(); + + // Segment with no PK terms. + doc = new Document(); + doc.add(new KeywordField("PK2", "3", Field.Store.NO)); + writer.addDocument(doc); + + doc = new Document(); + doc.add(new KeywordField("PK2", "4", Field.Store.NO)); + writer.addDocument(doc); + writer.flush(); + + DirectoryReader reader1 = DirectoryReader.open(writer); + PrimaryKeyLookup pk1 = new PrimaryKeyLookup(reader1, "PK"); + + doc = new Document(); + doc.add(new KeywordField("PK", "5", Field.Store.NO)); + writer.addDocument(doc); + + doc = new Document(); + doc.add(new KeywordField("PK", "6", Field.Store.NO)); + writer.addDocument(doc); + writer.deleteDocuments(new Term("PK", "1")); + writer.flush(); + + doc = new Document(); + doc.add(new KeywordField("PK2", "7", Field.Store.NO)); + writer.addDocument(doc); + + doc = new Document(); + doc.add(new KeywordField("PK2", "8", Field.Store.NO)); + writer.addDocument(doc); + writer.flush(); + + assertEquals(0, pk1.lookup(newBytesRef("1"))); + assertEquals(1, pk1.lookup(newBytesRef("2"))); + assertEquals(-1, pk1.lookup(newBytesRef("5"))); + assertEquals(-1, pk1.lookup(newBytesRef("8"))); + + DirectoryReader reader2 = DirectoryReader.openIfChanged(reader1); + PrimaryKeyLookup pk2 = pk1.reopen(reader2); + + assertEquals(-1, pk2.lookup(newBytesRef("1"))); + assertEquals(1, pk2.lookup(newBytesRef("2"))); + assertEquals(4, pk2.lookup(newBytesRef("5"))); + assertEquals(-1, pk2.lookup(newBytesRef("8"))); + + doc = new Document(); + doc.add(new KeywordField("PK", "9", Field.Store.NO)); + writer.addDocument(doc); + + doc = new Document(); + doc.add(new KeywordField("PK", "10", Field.Store.NO)); + writer.addDocument(doc); + writer.flush(); + + assertEquals(-1, pk2.lookup(newBytesRef("9"))); + DirectoryReader reader3 = DirectoryReader.openIfChanged(reader2); + PrimaryKeyLookup pk3 = pk2.reopen(reader3); + assertEquals(8, pk3.lookup(newBytesRef("9"))); + + DirectoryReader reader4 = DirectoryReader.openIfChanged(reader3); + assertNull(pk3.reopen(reader4)); + + writer.close(); + reader1.close(); + reader2.close(); + reader3.close(); + dir.close(); + } + + public void testPKLookupWithUpdate() throws Exception { + Directory dir = newDirectory(); + IndexWriter writer = + new IndexWriter( + dir, + new IndexWriterConfig(new MockAnalyzer(random())) + .setMergePolicy(NoMergePolicy.INSTANCE)); + + Document doc; + doc = new Document(); + doc.add(new KeywordField("PK", "1", Field.Store.NO)); + doc.add(new KeywordField("version", "1", Field.Store.NO)); + writer.addDocument(doc); + + doc = new Document(); + doc.add(new KeywordField("PK", "1", Field.Store.NO)); + doc.add(new KeywordField("version", "2", Field.Store.NO)); + writer.updateDocument(new Term("PK", "1"), doc); + + doc = new Document(); + doc.add(new KeywordField("PK", "1", Field.Store.NO)); + doc.add(new KeywordField("version", "3", Field.Store.NO)); + writer.updateDocument(new Term("PK", "1"), doc); + writer.flush(); + writer.close(); + + DirectoryReader reader = DirectoryReader.open(dir); + PrimaryKeyLookup pk = new PrimaryKeyLookup(reader, "PK"); + assertEquals(2, pk.lookup(newBytesRef("1"))); + + reader.close(); + dir.close(); + } + + /** + * Constructing on one thread and calling {@link PrimaryKeyLookup#lookup} from another must fire + * the thread-stickiness assertion. + */ + public void testThreadStickinessAssertion() throws Exception { + Directory dir = newDirectory(); + IndexWriter writer = new IndexWriter(dir, new IndexWriterConfig(new MockAnalyzer(random()))); + Document doc = new Document(); + doc.add(new KeywordField("PK", "1", Field.Store.NO)); + writer.addDocument(doc); + writer.commit(); + writer.close(); + + DirectoryReader reader = DirectoryReader.open(dir); + PrimaryKeyLookup pk = new PrimaryKeyLookup(reader, "PK"); + + if (TEST_ASSERTS_ENABLED) { Review Comment: What a fun origin story for this `boolean TEST_ASSERTS_ENABLED`: Long ago, when we always ran tests with asserts enabled, which is of course the expected way to run tests, we had a wild bug, missed on review and I think maybe released, where the functionality only worked correctly when `assert` was enabled! I.e. the `assert` had an important side-effect for the method. And we didn't catch the bug because we never ran tests without assertions... so now we do randomly disable assertions during tests, except I think it's only in our CI builds. Local dev `./gradlew test` always enable assertions. ########## lucene/CHANGES.txt: ########## @@ -119,6 +119,9 @@ New Features * GITHUB#15818: Add BM25 k3 query-term frequency saturation to BM25Similarity. (Sagar Upadhyaya) +* GITHUB#15520: Promote PerThreadPKLookup from the test-framework to core as Review Comment: Maybe also state that this class optimizes finding documents by their primary key value, faster than a simple `TermQuery` or so? ########## lucene/core/src/test/org/apache/lucene/index/TestPrimaryKeyLookup.java: ########## @@ -0,0 +1,196 @@ +/* + * 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.index; + +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.KeywordField; +import org.apache.lucene.store.Directory; +import org.apache.lucene.tests.analysis.MockAnalyzer; +import org.apache.lucene.tests.util.LuceneTestCase; + +public class TestPrimaryKeyLookup extends LuceneTestCase { + + public void testReopen() throws Exception { + Directory dir = newDirectory(); + IndexWriter writer = + new IndexWriter( + dir, + new IndexWriterConfig(new MockAnalyzer(random())) + .setMergePolicy(NoMergePolicy.INSTANCE)); + + Document doc; + doc = new Document(); + doc.add(new KeywordField("PK", "1", Field.Store.NO)); + writer.addDocument(doc); + + doc = new Document(); + doc.add(new KeywordField("PK", "2", Field.Store.NO)); + writer.addDocument(doc); + writer.flush(); + + // Segment with no PK terms. + doc = new Document(); + doc.add(new KeywordField("PK2", "3", Field.Store.NO)); + writer.addDocument(doc); + + doc = new Document(); + doc.add(new KeywordField("PK2", "4", Field.Store.NO)); + writer.addDocument(doc); + writer.flush(); + + DirectoryReader reader1 = DirectoryReader.open(writer); + PrimaryKeyLookup pk1 = new PrimaryKeyLookup(reader1, "PK"); + + doc = new Document(); + doc.add(new KeywordField("PK", "5", Field.Store.NO)); + writer.addDocument(doc); + + doc = new Document(); + doc.add(new KeywordField("PK", "6", Field.Store.NO)); + writer.addDocument(doc); + writer.deleteDocuments(new Term("PK", "1")); + writer.flush(); + + doc = new Document(); + doc.add(new KeywordField("PK2", "7", Field.Store.NO)); + writer.addDocument(doc); + + doc = new Document(); + doc.add(new KeywordField("PK2", "8", Field.Store.NO)); + writer.addDocument(doc); + writer.flush(); + + assertEquals(0, pk1.lookup(newBytesRef("1"))); + assertEquals(1, pk1.lookup(newBytesRef("2"))); + assertEquals(-1, pk1.lookup(newBytesRef("5"))); + assertEquals(-1, pk1.lookup(newBytesRef("8"))); + + DirectoryReader reader2 = DirectoryReader.openIfChanged(reader1); + PrimaryKeyLookup pk2 = pk1.reopen(reader2); + + assertEquals(-1, pk2.lookup(newBytesRef("1"))); + assertEquals(1, pk2.lookup(newBytesRef("2"))); + assertEquals(4, pk2.lookup(newBytesRef("5"))); + assertEquals(-1, pk2.lookup(newBytesRef("8"))); + + doc = new Document(); + doc.add(new KeywordField("PK", "9", Field.Store.NO)); + writer.addDocument(doc); + + doc = new Document(); + doc.add(new KeywordField("PK", "10", Field.Store.NO)); + writer.addDocument(doc); + writer.flush(); + + assertEquals(-1, pk2.lookup(newBytesRef("9"))); + DirectoryReader reader3 = DirectoryReader.openIfChanged(reader2); + PrimaryKeyLookup pk3 = pk2.reopen(reader3); + assertEquals(8, pk3.lookup(newBytesRef("9"))); + + DirectoryReader reader4 = DirectoryReader.openIfChanged(reader3); + assertNull(pk3.reopen(reader4)); + + writer.close(); + reader1.close(); + reader2.close(); + reader3.close(); + dir.close(); + } + + public void testPKLookupWithUpdate() throws Exception { + Directory dir = newDirectory(); + IndexWriter writer = + new IndexWriter( + dir, + new IndexWriterConfig(new MockAnalyzer(random())) + .setMergePolicy(NoMergePolicy.INSTANCE)); + + Document doc; + doc = new Document(); + doc.add(new KeywordField("PK", "1", Field.Store.NO)); + doc.add(new KeywordField("version", "1", Field.Store.NO)); + writer.addDocument(doc); + + doc = new Document(); + doc.add(new KeywordField("PK", "1", Field.Store.NO)); + doc.add(new KeywordField("version", "2", Field.Store.NO)); + writer.updateDocument(new Term("PK", "1"), doc); + + doc = new Document(); + doc.add(new KeywordField("PK", "1", Field.Store.NO)); + doc.add(new KeywordField("version", "3", Field.Store.NO)); + writer.updateDocument(new Term("PK", "1"), doc); + writer.flush(); + writer.close(); + + DirectoryReader reader = DirectoryReader.open(dir); + PrimaryKeyLookup pk = new PrimaryKeyLookup(reader, "PK"); + assertEquals(2, pk.lookup(newBytesRef("1"))); + + reader.close(); + dir.close(); + } + + /** + * Constructing on one thread and calling {@link PrimaryKeyLookup#lookup} from another must fire + * the thread-stickiness assertion. + */ + public void testThreadStickinessAssertion() throws Exception { + Directory dir = newDirectory(); + IndexWriter writer = new IndexWriter(dir, new IndexWriterConfig(new MockAnalyzer(random()))); + Document doc = new Document(); + doc.add(new KeywordField("PK", "1", Field.Store.NO)); + writer.addDocument(doc); + writer.commit(); + writer.close(); + + DirectoryReader reader = DirectoryReader.open(dir); + PrimaryKeyLookup pk = new PrimaryKeyLookup(reader, "PK"); + + if (TEST_ASSERTS_ENABLED) { + // pk was constructed on this thread; calling lookup from any other thread must trip + // the thread-stickiness assertion in PrimaryKeyLookup. + class CrossThreadLookup extends Thread { + Throwable thrown; Review Comment: Add `volatile`? (Mike in [JMM paranoia mode](https://en.wikipedia.org/wiki/Java_memory_model)). -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
