From 676be86e8fda71f63bbc30527dc22cbbb6fc360c Mon Sep 17 00:00:00 2001 From: Dawid Weiss Date: Tue, 18 Jun 2024 09:43:29 +0200 Subject: [PATCH 01/42] Correct the default checksum for artifacts to be sha512. #13484 --- .../src/main/java/org/apache/lucene/gradle/Checksum.java | 1 - 1 file changed, 1 deletion(-) diff --git a/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/Checksum.java b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/Checksum.java index 49374214c9c..7566102294f 100644 --- a/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/Checksum.java +++ b/build-tools/build-infra/src/main/java/org/apache/lucene/gradle/Checksum.java @@ -68,7 +68,6 @@ public class Checksum extends DefaultTask { public Checksum() { outputDir = new File(getProject().getBuildDir(), "checksums"); - algorithm = Algorithm.SHA256; } @InputFiles From 04def262894ea37d421614c7b883b3011146f2af Mon Sep 17 00:00:00 2001 From: Dawid Weiss Date: Tue, 18 Jun 2024 09:57:19 +0200 Subject: [PATCH 02/42] Smoke tester should no longer expect buildSrc at the top level. #13484 --- dev-tools/scripts/smokeTestRelease.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-tools/scripts/smokeTestRelease.py b/dev-tools/scripts/smokeTestRelease.py index 87e70adbb1f..ebc7f030162 100755 --- a/dev-tools/scripts/smokeTestRelease.py +++ b/dev-tools/scripts/smokeTestRelease.py @@ -582,7 +582,7 @@ def verifyUnpacked(java, artifact, unpackPath, gitRevision, version, testArgs): 'luke', 'memory', 'misc', 'monitor', 'queries', 'queryparser', 'replicator', 'sandbox', 'spatial-extras', 'spatial-test-fixtures', 'spatial3d', 'suggest', 'test-framework', 'licenses'] if isSrc: - expected_src_root_files = ['build.gradle', 'buildSrc', 'CONTRIBUTING.md', 'dev-docs', 'dev-tools', 'gradle', 'gradlew', + expected_src_root_files = ['build.gradle', 'CONTRIBUTING.md', 'dev-docs', 'dev-tools', 'gradle', 'gradlew', 'gradlew.bat', 'help', 'lucene', 'settings.gradle', 'versions.lock', 'versions.props'] expected_src_lucene_files = ['build.gradle', 'documentation', 'distribution', 'dev-docs'] is_in_list(in_root_folder, expected_src_root_files) From bff0b1daeea8952c9d76e61c060fccb0bb95aed8 Mon Sep 17 00:00:00 2001 From: Dawid Weiss Date: Tue, 18 Jun 2024 10:17:38 +0200 Subject: [PATCH 03/42] Smoke tester should no longer expect versions.props and look for versions.toml instead. #13484 --- dev-tools/scripts/smokeTestRelease.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-tools/scripts/smokeTestRelease.py b/dev-tools/scripts/smokeTestRelease.py index ebc7f030162..6e9107b8b4c 100755 --- a/dev-tools/scripts/smokeTestRelease.py +++ b/dev-tools/scripts/smokeTestRelease.py @@ -583,7 +583,7 @@ def verifyUnpacked(java, artifact, unpackPath, gitRevision, version, testArgs): 'sandbox', 'spatial-extras', 'spatial-test-fixtures', 'spatial3d', 'suggest', 'test-framework', 'licenses'] if isSrc: expected_src_root_files = ['build.gradle', 'CONTRIBUTING.md', 'dev-docs', 'dev-tools', 'gradle', 'gradlew', - 'gradlew.bat', 'help', 'lucene', 'settings.gradle', 'versions.lock', 'versions.props'] + 'gradlew.bat', 'help', 'lucene', 'settings.gradle', 'versions.lock', 'versions.toml'] expected_src_lucene_files = ['build.gradle', 'documentation', 'distribution', 'dev-docs'] is_in_list(in_root_folder, expected_src_root_files) is_in_list(in_lucene_folder, expected_folders) From ff3fe75adb215b782055354a1fa3f987e369b54a Mon Sep 17 00:00:00 2001 From: Dawid Weiss Date: Tue, 18 Jun 2024 10:33:53 +0200 Subject: [PATCH 04/42] Add build-tools to smoke tester's expect list. #13484 --- dev-tools/scripts/smokeTestRelease.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-tools/scripts/smokeTestRelease.py b/dev-tools/scripts/smokeTestRelease.py index 6e9107b8b4c..82946f4e5b3 100755 --- a/dev-tools/scripts/smokeTestRelease.py +++ b/dev-tools/scripts/smokeTestRelease.py @@ -582,7 +582,7 @@ def verifyUnpacked(java, artifact, unpackPath, gitRevision, version, testArgs): 'luke', 'memory', 'misc', 'monitor', 'queries', 'queryparser', 'replicator', 'sandbox', 'spatial-extras', 'spatial-test-fixtures', 'spatial3d', 'suggest', 'test-framework', 'licenses'] if isSrc: - expected_src_root_files = ['build.gradle', 'CONTRIBUTING.md', 'dev-docs', 'dev-tools', 'gradle', 'gradlew', + expected_src_root_files = ['build.gradle', 'build-tools', 'CONTRIBUTING.md', 'dev-docs', 'dev-tools', 'gradle', 'gradlew', 'gradlew.bat', 'help', 'lucene', 'settings.gradle', 'versions.lock', 'versions.toml'] expected_src_lucene_files = ['build.gradle', 'documentation', 'distribution', 'dev-docs'] is_in_list(in_root_folder, expected_src_root_files) From 7e31f56ea1130909948bcf9e5beefc161ceef137 Mon Sep 17 00:00:00 2001 From: Dawid Weiss Date: Tue, 18 Jun 2024 14:25:40 +0200 Subject: [PATCH 05/42] Add versions.toml to .gitattributes and normalize line endings to lf. #13484 --- .gitattributes | 4 +- versions.toml | 170 ++++++++++++++++++++++++------------------------- 2 files changed, 87 insertions(+), 87 deletions(-) diff --git a/.gitattributes b/.gitattributes index e4f4bf8b496..a3135003e80 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,6 @@ # Ignore all differences in line endings for the lock file. -versions.lock text eol=lf -versions.props text eol=lf +versions.lock text eol=lf +versions.toml text eol=lf # Gradle files are always in LF. *.gradle text eol=lf diff --git a/versions.toml b/versions.toml index 55f59301781..6a137975d3e 100644 --- a/versions.toml +++ b/versions.toml @@ -1,85 +1,85 @@ -[versions] -antlr = "4.11.1" -asm = "9.6" -assertj = "3.21.0" -commons-codec = "1.13" -commons-compress = "1.19" -ecj = "3.36.0" -errorprone = "2.18.0" -flexmark = "0.61.24" -# @keep This is GJF version for spotless/ tidy. -googleJavaFormat = "1.18.1" -groovy = "3.0.21" -hamcrest = "2.2" -icu4j = "74.2" -javacc = "7.0.12" -jflex = "1.8.2" -jgit = "5.13.1.202206130422-r" -jmh = "1.37" -jts = "1.17.0" -junit = "4.13.1" -# @keep Minimum gradle version to run the build -minGradle = "8.8" -# @keep This is the minimum required Java version. -minJava = "21" -morfologik = "2.1.9" -morfologik-ukrainian = "4.9.1" -nekohtml = "1.9.17" -opennlp = "2.3.2" -procfork = "1.0.6" -randomizedtesting = "2.8.1" -rat = "0.14" -s2-geometry = "1.0.0" -spatial4j = "0.8" -xerces = "2.12.0" -zstd = "1.5.5-11" - -[libraries] -antlr-core = { module = "org.antlr:antlr4", version.ref = "antlr" } -antlr-runtime = { module = "org.antlr:antlr4-runtime", version.ref = "antlr" } -asm-commons = { module = "org.ow2.asm:asm-commons", version.ref = "asm" } -asm-core = { module = "org.ow2.asm:asm", version.ref = "asm" } -assertj = { module = "org.assertj:assertj-core", version.ref = "assertj" } -commons-codec = { module = "commons-codec:commons-codec", version.ref = "commons-codec" } -commons-compress = { module = "org.apache.commons:commons-compress", version.ref = "commons-compress" } -ecj = { module = "org.eclipse.jdt:ecj", version.ref = "ecj" } -errorprone = { module = "com.google.errorprone:error_prone_core", version.ref = "errorprone" } -flexmark-core = { module = "com.vladsch.flexmark:flexmark", version.ref = "flexmark" } -flexmark-ext-abbreviation = { module = "com.vladsch.flexmark:flexmark-ext-abbreviation", version.ref = "flexmark" } -flexmark-ext-attributes = { module = "com.vladsch.flexmark:flexmark-ext-attributes", version.ref = "flexmark" } -flexmark-ext-autolink = { module = "com.vladsch.flexmark:flexmark-ext-autolink", version.ref = "flexmark" } -flexmark-ext-tables = { module = "com.vladsch.flexmark:flexmark-ext-tables", version.ref = "flexmark" } -groovy = { module = "org.codehaus.groovy:groovy-all", version.ref = "groovy" } -hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "hamcrest" } -icu4j = { module = "com.ibm.icu:icu4j", version.ref = "icu4j" } -javacc = { module = "net.java.dev.javacc:javacc", version.ref = "javacc" } -jflex = { module = "de.jflex:jflex", version.ref = "jflex" } -jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version.ref = "jgit" } -jmh-annprocess = { module = "org.openjdk.jmh:jmh-generator-annprocess", version.ref = "jmh" } -jmh-core = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" } -jts = { module = "org.locationtech.jts:jts-core", version.ref = "jts" } -junit = { module = "junit:junit", version.ref = "junit" } -morfologik-polish = { module = "org.carrot2:morfologik-polish", version.ref = "morfologik" } -morfologik-stemming = { module = "org.carrot2:morfologik-stemming", version.ref = "morfologik" } -morfologik-ukrainian = { module = "ua.net.nlp:morfologik-ukrainian-search", version.ref = "morfologik-ukrainian" } -nekohtml = { module = "net.sourceforge.nekohtml:nekohtml", version.ref = "nekohtml" } -opennlp-tools = { module = "org.apache.opennlp:opennlp-tools", version.ref = "opennlp" } -procfork = { module = "com.carrotsearch:procfork", version.ref = "procfork" } -randomizedtesting-runner = { module = "com.carrotsearch.randomizedtesting:randomizedtesting-runner", version.ref = "randomizedtesting" } -rat = { module = "org.apache.rat:apache-rat", version.ref = "rat" } -s2-geometry = { module = "io.sgr:s2-geometry-library-java", version.ref = "s2-geometry" } -spatial4j = { module = "org.locationtech.spatial4j:spatial4j", version.ref = "spatial4j" } -xerces = { module = "xerces:xercesImpl", version.ref = "xerces" } -zstd = { module = "com.github.luben:zstd-jni", version.ref = "zstd" } - -[plugins] -benmanes-versions = "com.github.ben-manes.versions:0.51.0" -dependencychecks = "com.carrotsearch.gradle.dependencychecks:0.0.9" -errorprone = "net.ltgt.errorprone:3.1.0" -forbiddenapis = "de.thetaphi.forbiddenapis:3.7" -jacocolog = "org.barfuin.gradle.jacocolog:3.1.0" -owasp-dependencycheck = "org.owasp.dependencycheck:7.2.0" -randomizedtesting = "com.carrotsearch.gradle.randomizedtesting:0.0.6" -spotless = "com.diffplug.spotless:6.5.2" -undercouch-download = "de.undercouch.download:5.2.0" -versionCatalogUpdate = "nl.littlerobots.version-catalog-update:0.8.4" +[versions] +antlr = "4.11.1" +asm = "9.6" +assertj = "3.21.0" +commons-codec = "1.13" +commons-compress = "1.19" +ecj = "3.36.0" +errorprone = "2.18.0" +flexmark = "0.61.24" +# @keep This is GJF version for spotless/ tidy. +googleJavaFormat = "1.18.1" +groovy = "3.0.21" +hamcrest = "2.2" +icu4j = "74.2" +javacc = "7.0.12" +jflex = "1.8.2" +jgit = "5.13.1.202206130422-r" +jmh = "1.37" +jts = "1.17.0" +junit = "4.13.1" +# @keep Minimum gradle version to run the build +minGradle = "8.8" +# @keep This is the minimum required Java version. +minJava = "21" +morfologik = "2.1.9" +morfologik-ukrainian = "4.9.1" +nekohtml = "1.9.17" +opennlp = "2.3.2" +procfork = "1.0.6" +randomizedtesting = "2.8.1" +rat = "0.14" +s2-geometry = "1.0.0" +spatial4j = "0.8" +xerces = "2.12.0" +zstd = "1.5.5-11" + +[libraries] +antlr-core = { module = "org.antlr:antlr4", version.ref = "antlr" } +antlr-runtime = { module = "org.antlr:antlr4-runtime", version.ref = "antlr" } +asm-commons = { module = "org.ow2.asm:asm-commons", version.ref = "asm" } +asm-core = { module = "org.ow2.asm:asm", version.ref = "asm" } +assertj = { module = "org.assertj:assertj-core", version.ref = "assertj" } +commons-codec = { module = "commons-codec:commons-codec", version.ref = "commons-codec" } +commons-compress = { module = "org.apache.commons:commons-compress", version.ref = "commons-compress" } +ecj = { module = "org.eclipse.jdt:ecj", version.ref = "ecj" } +errorprone = { module = "com.google.errorprone:error_prone_core", version.ref = "errorprone" } +flexmark-core = { module = "com.vladsch.flexmark:flexmark", version.ref = "flexmark" } +flexmark-ext-abbreviation = { module = "com.vladsch.flexmark:flexmark-ext-abbreviation", version.ref = "flexmark" } +flexmark-ext-attributes = { module = "com.vladsch.flexmark:flexmark-ext-attributes", version.ref = "flexmark" } +flexmark-ext-autolink = { module = "com.vladsch.flexmark:flexmark-ext-autolink", version.ref = "flexmark" } +flexmark-ext-tables = { module = "com.vladsch.flexmark:flexmark-ext-tables", version.ref = "flexmark" } +groovy = { module = "org.codehaus.groovy:groovy-all", version.ref = "groovy" } +hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "hamcrest" } +icu4j = { module = "com.ibm.icu:icu4j", version.ref = "icu4j" } +javacc = { module = "net.java.dev.javacc:javacc", version.ref = "javacc" } +jflex = { module = "de.jflex:jflex", version.ref = "jflex" } +jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version.ref = "jgit" } +jmh-annprocess = { module = "org.openjdk.jmh:jmh-generator-annprocess", version.ref = "jmh" } +jmh-core = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" } +jts = { module = "org.locationtech.jts:jts-core", version.ref = "jts" } +junit = { module = "junit:junit", version.ref = "junit" } +morfologik-polish = { module = "org.carrot2:morfologik-polish", version.ref = "morfologik" } +morfologik-stemming = { module = "org.carrot2:morfologik-stemming", version.ref = "morfologik" } +morfologik-ukrainian = { module = "ua.net.nlp:morfologik-ukrainian-search", version.ref = "morfologik-ukrainian" } +nekohtml = { module = "net.sourceforge.nekohtml:nekohtml", version.ref = "nekohtml" } +opennlp-tools = { module = "org.apache.opennlp:opennlp-tools", version.ref = "opennlp" } +procfork = { module = "com.carrotsearch:procfork", version.ref = "procfork" } +randomizedtesting-runner = { module = "com.carrotsearch.randomizedtesting:randomizedtesting-runner", version.ref = "randomizedtesting" } +rat = { module = "org.apache.rat:apache-rat", version.ref = "rat" } +s2-geometry = { module = "io.sgr:s2-geometry-library-java", version.ref = "s2-geometry" } +spatial4j = { module = "org.locationtech.spatial4j:spatial4j", version.ref = "spatial4j" } +xerces = { module = "xerces:xercesImpl", version.ref = "xerces" } +zstd = { module = "com.github.luben:zstd-jni", version.ref = "zstd" } + +[plugins] +benmanes-versions = "com.github.ben-manes.versions:0.51.0" +dependencychecks = "com.carrotsearch.gradle.dependencychecks:0.0.9" +errorprone = "net.ltgt.errorprone:3.1.0" +forbiddenapis = "de.thetaphi.forbiddenapis:3.7" +jacocolog = "org.barfuin.gradle.jacocolog:3.1.0" +owasp-dependencycheck = "org.owasp.dependencycheck:7.2.0" +randomizedtesting = "com.carrotsearch.gradle.randomizedtesting:0.0.6" +spotless = "com.diffplug.spotless:6.5.2" +undercouch-download = "de.undercouch.download:5.2.0" +versionCatalogUpdate = "nl.littlerobots.version-catalog-update:0.8.4" From 937c004edaa7da1f16d4e6885bcb53259080f3bf Mon Sep 17 00:00:00 2001 From: Greg Miller Date: Tue, 18 Jun 2024 18:34:15 -0700 Subject: [PATCH 06/42] Fix global score update bug in MultiLeafKnnCollector (#13463) --- lucene/CHANGES.txt | 4 +- .../search/knn/MultiLeafKnnCollector.java | 17 ++++- .../lucene/util/hnsw/BlockingFloatHeap.java | 5 +- .../search/knn/TestMultiLeafKnnCollector.java | 62 +++++++++++++++++++ 4 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 lucene/core/src/test/org/apache/lucene/search/knn/TestMultiLeafKnnCollector.java diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index f6ee2edbde8..baa53ed5d90 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -279,7 +279,9 @@ Optimizations Bug Fixes --------------------- -(No changes) + +* GITHUB#13463: Address bug in MultiLeafKnnCollector causing #minCompetitiveSimilarity to stay artificially low in + some corner cases. (Greg Miller) Other -------------------- diff --git a/lucene/core/src/java/org/apache/lucene/search/knn/MultiLeafKnnCollector.java b/lucene/core/src/java/org/apache/lucene/search/knn/MultiLeafKnnCollector.java index 5f7b26f95d4..1ca979d6794 100644 --- a/lucene/core/src/java/org/apache/lucene/search/knn/MultiLeafKnnCollector.java +++ b/lucene/core/src/java/org/apache/lucene/search/knn/MultiLeafKnnCollector.java @@ -41,6 +41,7 @@ public final class MultiLeafKnnCollector implements KnnCollector { private final float greediness; // the queue of the local similarities to periodically update with the global queue private final FloatHeap updatesQueue; + private final float[] updatesScratch; // interval to synchronize the local and global queues, as a number of visited vectors private final int interval = 0xff; // 255 private boolean kResultsCollected = false; @@ -62,6 +63,7 @@ public final class MultiLeafKnnCollector implements KnnCollector { this.globalSimilarityQueue = globalSimilarityQueue; this.nonCompetitiveQueue = new FloatHeap(Math.max(1, Math.round((1 - greediness) * k))); this.updatesQueue = new FloatHeap(k); + this.updatesScratch = new float[k]; } @Override @@ -103,9 +105,18 @@ public final class MultiLeafKnnCollector implements KnnCollector { if (kResultsCollected) { // as we've collected k results, we can start do periodic updates with the global queue if (firstKResultsCollected || (subCollector.visitedCount() & interval) == 0) { - cachedGlobalMinSim = globalSimilarityQueue.offer(updatesQueue.getHeap()); - updatesQueue.clear(); - globalSimUpdated = true; + // BlockingFloatHeap#offer requires input to be sorted in ascending order, so we can't + // pass in the underlying updatesQueue array as-is since it is only partially ordered + // (see GH#13462): + int len = updatesQueue.size(); + if (len > 0) { + for (int i = 0; i < len; i++) { + updatesScratch[i] = updatesQueue.poll(); + } + assert updatesQueue.size() == 0; + cachedGlobalMinSim = globalSimilarityQueue.offer(updatesScratch, len); + globalSimUpdated = true; + } } } return localSimUpdated || globalSimUpdated; diff --git a/lucene/core/src/java/org/apache/lucene/util/hnsw/BlockingFloatHeap.java b/lucene/core/src/java/org/apache/lucene/util/hnsw/BlockingFloatHeap.java index a81eaf2fee0..6bbf6fdb741 100644 --- a/lucene/core/src/java/org/apache/lucene/util/hnsw/BlockingFloatHeap.java +++ b/lucene/core/src/java/org/apache/lucene/util/hnsw/BlockingFloatHeap.java @@ -72,12 +72,13 @@ public final class BlockingFloatHeap { *

Values must be sorted in ascending order. * * @param values a set of values to insert, must be sorted in ascending order + * @param len number of values from the {@code values} array to insert * @return the new 'top' element in the queue. */ - public float offer(float[] values) { + public float offer(float[] values, int len) { lock.lock(); try { - for (int i = values.length - 1; i >= 0; i--) { + for (int i = len - 1; i >= 0; i--) { if (size < maxSize) { push(values[i]); } else { diff --git a/lucene/core/src/test/org/apache/lucene/search/knn/TestMultiLeafKnnCollector.java b/lucene/core/src/test/org/apache/lucene/search/knn/TestMultiLeafKnnCollector.java new file mode 100644 index 00000000000..8466293f61a --- /dev/null +++ b/lucene/core/src/test/org/apache/lucene/search/knn/TestMultiLeafKnnCollector.java @@ -0,0 +1,62 @@ +/* + * 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.knn; + +import org.apache.lucene.search.TopKnnCollector; +import org.apache.lucene.tests.util.LuceneTestCase; +import org.apache.lucene.util.hnsw.BlockingFloatHeap; + +public class TestMultiLeafKnnCollector extends LuceneTestCase { + + /** Validates a fix for GH#13462 */ + public void testGlobalScoreCoordination() { + int k = 7; + BlockingFloatHeap globalHeap = new BlockingFloatHeap(k); + MultiLeafKnnCollector collector1 = + new MultiLeafKnnCollector(k, globalHeap, new TopKnnCollector(k, Integer.MAX_VALUE)); + MultiLeafKnnCollector collector2 = + new MultiLeafKnnCollector(k, globalHeap, new TopKnnCollector(k, Integer.MAX_VALUE)); + + // Collect k (7) hits in collector1 with scores [100, 106]: + for (int i = 0; i < k; i++) { + collector1.collect(0, 100f + i); + } + + // The global heap should be updated since k hits were collected, and have a min score of + // 100: + assertEquals(100f, globalHeap.peek(), 0f); + assertEquals(100f, collector1.minCompetitiveSimilarity(), 0f); + + // Collect k (7) hits in collector2 with only two that are competitive (200 and 300), + // which also forces an update of the global heap with collector2's hits. This is a tricky + // case where the heap will not be fully ordered, so it ensures global queue updates don't + // incorrectly short-circuit (see GH#13462): + collector2.collect(0, 10f); + collector2.collect(0, 11f); + collector2.collect(0, 12f); + collector2.collect(0, 13f); + collector2.collect(0, 200f); + collector2.collect(0, 14f); + collector2.collect(0, 300f); + + // At this point, our global heap should contain [102, 103, 104, 105, 106, 200, 300] since + // values 200 and 300 from collector2 should have pushed out 100 and 101 from collector1. + // The min value on the global heap should be 102: + assertEquals(102f, globalHeap.peek(), 0f); + assertEquals(102f, collector2.minCompetitiveSimilarity(), 0f); + } +} From 057cbf3c8665354cd21dac20570a7e088dc8ce3a Mon Sep 17 00:00:00 2001 From: zhouhui Date: Wed, 19 Jun 2024 17:53:12 +0800 Subject: [PATCH 07/42] Use getAndSet, getAndClear instead split operations. (#13507) --- .../org/apache/lucene/index/FreqProxTermsWriter.java | 3 +-- .../src/java/org/apache/lucene/index/PendingDeletes.java | 3 +-- .../java/org/apache/lucene/index/PendingSoftDeletes.java | 9 +++------ .../search/join/PointInSetIncludingScoreQuery.java | 3 +-- .../lucene/search/join/TermsIncludingScoreQuery.java | 3 +-- 5 files changed, 7 insertions(+), 14 deletions(-) diff --git a/lucene/core/src/java/org/apache/lucene/index/FreqProxTermsWriter.java b/lucene/core/src/java/org/apache/lucene/index/FreqProxTermsWriter.java index f04096c78cf..5f6abd69c29 100644 --- a/lucene/core/src/java/org/apache/lucene/index/FreqProxTermsWriter.java +++ b/lucene/core/src/java/org/apache/lucene/index/FreqProxTermsWriter.java @@ -70,9 +70,8 @@ final class FreqProxTermsWriter extends TermsHash { state.liveDocs = new FixedBitSet(state.segmentInfo.maxDoc()); state.liveDocs.set(0, state.segmentInfo.maxDoc()); } - if (state.liveDocs.get(doc)) { + if (state.liveDocs.getAndClear(doc)) { state.delCountOnFlush++; - state.liveDocs.clear(doc); } } } diff --git a/lucene/core/src/java/org/apache/lucene/index/PendingDeletes.java b/lucene/core/src/java/org/apache/lucene/index/PendingDeletes.java index 513f897d725..4be3e41ad19 100644 --- a/lucene/core/src/java/org/apache/lucene/index/PendingDeletes.java +++ b/lucene/core/src/java/org/apache/lucene/index/PendingDeletes.java @@ -97,9 +97,8 @@ class PendingDeletes { + info.info.name + " maxDoc=" + info.info.maxDoc(); - final boolean didDelete = mutableBits.get(docID); + final boolean didDelete = mutableBits.getAndClear(docID); if (didDelete) { - mutableBits.clear(docID); pendingDeleteCount++; } return didDelete; diff --git a/lucene/core/src/java/org/apache/lucene/index/PendingSoftDeletes.java b/lucene/core/src/java/org/apache/lucene/index/PendingSoftDeletes.java index accdb57d1ba..557d31ad441 100644 --- a/lucene/core/src/java/org/apache/lucene/index/PendingSoftDeletes.java +++ b/lucene/core/src/java/org/apache/lucene/index/PendingSoftDeletes.java @@ -53,8 +53,7 @@ final class PendingSoftDeletes extends PendingDeletes { FixedBitSet mutableBits = getMutableBits(); // hardDeletes if (hardDeletes.delete(docID)) { - if (mutableBits.get(docID)) { // delete it here too! - mutableBits.clear(docID); + if (mutableBits.getAndClear(docID)) { // delete it here too! assert hardDeletes.delete(docID) == false; } else { // if it was deleted subtract the delCount @@ -135,16 +134,14 @@ final class PendingSoftDeletes extends PendingDeletes { : null; while ((docID = iterator.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) { if (hasValue == null || hasValue.hasValue()) { - if (bits.get(docID)) { // doc is live - clear it - bits.clear(docID); + if (bits.getAndClear(docID)) { // doc is live - clear it newDeletes++; // now that we know we deleted it and we fully control the hard deletes we can do correct // accounting // below. } } else { - if (bits.get(docID) == false) { - bits.set(docID); + if (bits.getAndSet(docID) == false) { newDeletes--; } } diff --git a/lucene/join/src/java/org/apache/lucene/search/join/PointInSetIncludingScoreQuery.java b/lucene/join/src/java/org/apache/lucene/search/join/PointInSetIncludingScoreQuery.java index 1ece42078cf..717acc22800 100644 --- a/lucene/join/src/java/org/apache/lucene/search/join/PointInSetIncludingScoreQuery.java +++ b/lucene/join/src/java/org/apache/lucene/search/join/PointInSetIncludingScoreQuery.java @@ -276,8 +276,7 @@ abstract class PointInSetIncludingScoreQuery extends Query implements Accountabl if (cmp == 0) { // Query point equals index point, so collect and return if (multipleValuesPerDocument) { - if (result.get(docID) == false) { - result.set(docID); + if (result.getAndSet(docID) == false) { scores[docID] = nextScore; } } else { diff --git a/lucene/join/src/java/org/apache/lucene/search/join/TermsIncludingScoreQuery.java b/lucene/join/src/java/org/apache/lucene/search/join/TermsIncludingScoreQuery.java index a3fa1685844..c132c9f1a56 100644 --- a/lucene/join/src/java/org/apache/lucene/search/join/TermsIncludingScoreQuery.java +++ b/lucene/join/src/java/org/apache/lucene/search/join/TermsIncludingScoreQuery.java @@ -281,9 +281,8 @@ class TermsIncludingScoreQuery extends Query implements Accountable { matchingDocs.set(doc); }*/ // But this behaves the same as MVInnerScorer and only then the tests will pass: - if (!matchingDocs.get(doc)) { + if (!matchingDocs.getAndSet(doc)) { scores[doc] = score; - matchingDocs.set(doc); } } } From d453832bb8d5318a444053240f93bff17dc680d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Campinas?= Date: Thu, 20 Jun 2024 16:36:51 +0200 Subject: [PATCH 08/42] Fix IndexOutOfBoundsException thrown in DefaultPassageFormatter by unordered matches (#13315) --- lucene/CHANGES.txt | 3 ++ .../search/DisjunctionMatchesIterator.java | 9 ++++ .../apache/lucene/search/MatchesIterator.java | 4 +- .../TestUnifiedHighlighterTermVec.java | 47 +++++++++++++++++++ 4 files changed, 61 insertions(+), 2 deletions(-) diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index baa53ed5d90..13c7c2dcb3b 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -179,6 +179,9 @@ Bug Fixes * GITHUB#12878: Fix the declared Exceptions of Expression#evaluate() to match those of DoubleValues#doubleValue(). (Uwe Schindler) +* GITHUB#12431: Fix IndexOutOfBoundsException thrown in DefaultPassageFormatter + by unordered matches. (Stephane Campinas) + Changes in Runtime Behavior --------------------- diff --git a/lucene/core/src/java/org/apache/lucene/search/DisjunctionMatchesIterator.java b/lucene/core/src/java/org/apache/lucene/search/DisjunctionMatchesIterator.java index fa86505088d..13852b1b9ee 100644 --- a/lucene/core/src/java/org/apache/lucene/search/DisjunctionMatchesIterator.java +++ b/lucene/core/src/java/org/apache/lucene/search/DisjunctionMatchesIterator.java @@ -194,6 +194,15 @@ final class DisjunctionMatchesIterator implements MatchesIterator { new PriorityQueue(matches.size()) { @Override protected boolean lessThan(MatchesIterator a, MatchesIterator b) { + if (a.startPosition() == -1 && b.startPosition() == -1) { + try { + return a.startOffset() < b.startOffset() + || (a.startOffset() == b.startOffset() && a.endOffset() < b.endOffset()) + || (a.startOffset() == b.startOffset() && a.endOffset() == b.endOffset()); + } catch (IOException e) { + throw new IllegalArgumentException("Failed to retrieve term offset", e); + } + } return a.startPosition() < b.startPosition() || (a.startPosition() == b.startPosition() && a.endPosition() < b.endPosition()) || (a.startPosition() == b.startPosition() && a.endPosition() == b.endPosition()); diff --git a/lucene/core/src/java/org/apache/lucene/search/MatchesIterator.java b/lucene/core/src/java/org/apache/lucene/search/MatchesIterator.java index 371ba0ee695..24da0246c6d 100644 --- a/lucene/core/src/java/org/apache/lucene/search/MatchesIterator.java +++ b/lucene/core/src/java/org/apache/lucene/search/MatchesIterator.java @@ -45,14 +45,14 @@ public interface MatchesIterator { boolean next() throws IOException; /** - * The start position of the current match + * The start position of the current match, or {@code -1} if positions are not available * *

Should only be called after {@link #next()} has returned {@code true} */ int startPosition(); /** - * The end position of the current match + * The end position of the current match, or {@code -1} if positions are not available * *

Should only be called after {@link #next()} has returned {@code true} */ diff --git a/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestUnifiedHighlighterTermVec.java b/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestUnifiedHighlighterTermVec.java index 35a638e2aa1..84ea233affe 100644 --- a/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestUnifiedHighlighterTermVec.java +++ b/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestUnifiedHighlighterTermVec.java @@ -27,6 +27,7 @@ import java.util.Set; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.FieldType; +import org.apache.lucene.document.TextField; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.Fields; import org.apache.lucene.index.FilterDirectoryReader; @@ -57,6 +58,52 @@ public class TestUnifiedHighlighterTermVec extends UnifiedHighlighterTestBase { super(randomFieldType(random())); } + public void testTermVecButNoPositions1() throws Exception { + testTermVecButNoPositions("x", "y", "y x", "y x"); + } + + public void testTermVecButNoPositions2() throws Exception { + testTermVecButNoPositions("y", "x", "y x", "y x"); + } + + public void testTermVecButNoPositions3() throws Exception { + testTermVecButNoPositions("zzz", "yyy", "zzz yyy", "zzz yyy"); + } + + public void testTermVecButNoPositions4() throws Exception { + testTermVecButNoPositions("zzz", "yyy", "yyy zzz", "yyy zzz"); + } + + public void testTermVecButNoPositions(String aaa, String bbb, String indexed, String expected) + throws Exception { + final FieldType tvNoPosType = new FieldType(TextField.TYPE_STORED); + tvNoPosType.setStoreTermVectors(true); + tvNoPosType.setStoreTermVectorOffsets(true); + tvNoPosType.freeze(); + + RandomIndexWriter iw = new RandomIndexWriter(random(), dir, indexAnalyzer); + + Field body = new Field("body", indexed, tvNoPosType); + Document document = new Document(); + document.add(body); + iw.addDocument(document); + try (IndexReader ir = iw.getReader()) { + iw.close(); + IndexSearcher searcher = newSearcher(ir); + BooleanQuery query = + new BooleanQuery.Builder() + .add(new TermQuery(new Term("body", aaa)), BooleanClause.Occur.MUST) + .add(new TermQuery(new Term("body", bbb)), BooleanClause.Occur.MUST) + .build(); + TopDocs topDocs = searcher.search(query, 10); + assertEquals(1, topDocs.totalHits.value); + UnifiedHighlighter highlighter = UnifiedHighlighter.builder(searcher, indexAnalyzer).build(); + String[] snippets = highlighter.highlight("body", query, topDocs, 2); + assertEquals(1, snippets.length); + assertTrue(snippets[0], snippets[0].contains(expected)); + } + } + public void testFetchTermVecsOncePerDoc() throws IOException { RandomIndexWriter iw = new RandomIndexWriter(random(), dir, indexAnalyzer); From 2529075d4879cc459a54f89527faa56844d26ceb Mon Sep 17 00:00:00 2001 From: Dawid Weiss Date: Thu, 20 Jun 2024 16:41:11 +0200 Subject: [PATCH 09/42] Moved changes entry to 9.12 (with the possibility of backporting to 9.11.1) #13315 --- lucene/CHANGES.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 13c7c2dcb3b..8a9b6f75811 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -179,9 +179,6 @@ Bug Fixes * GITHUB#12878: Fix the declared Exceptions of Expression#evaluate() to match those of DoubleValues#doubleValue(). (Uwe Schindler) -* GITHUB#12431: Fix IndexOutOfBoundsException thrown in DefaultPassageFormatter - by unordered matches. (Stephane Campinas) - Changes in Runtime Behavior --------------------- @@ -286,6 +283,9 @@ Bug Fixes * GITHUB#13463: Address bug in MultiLeafKnnCollector causing #minCompetitiveSimilarity to stay artificially low in some corner cases. (Greg Miller) +* GITHUB#12431: Fix IndexOutOfBoundsException thrown in DefaultPassageFormatter + by unordered matches. (Stephane Campinas) + Other -------------------- (No changes) From ab291210db08276fef1b52bf7b840d431e96ce08 Mon Sep 17 00:00:00 2001 From: Dawid Weiss Date: Thu, 20 Jun 2024 16:49:55 +0200 Subject: [PATCH 10/42] Removed changes entry for #13315 entirely as it'll appear in 9.11.1 section, once released. --- lucene/CHANGES.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 8a9b6f75811..baa53ed5d90 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -283,9 +283,6 @@ Bug Fixes * GITHUB#13463: Address bug in MultiLeafKnnCollector causing #minCompetitiveSimilarity to stay artificially low in some corner cases. (Greg Miller) -* GITHUB#12431: Fix IndexOutOfBoundsException thrown in DefaultPassageFormatter - by unordered matches. (Stephane Campinas) - Other -------------------- (No changes) From c7f4b8dee20c694ccfb8f26d8884edcbff5f4210 Mon Sep 17 00:00:00 2001 From: yadda yadda yadda Date: Fri, 21 Jun 2024 11:18:39 +0200 Subject: [PATCH 11/42] Fix NPE in StringValueFacetCounts over empty match-set (#13494) --- lucene/CHANGES.txt | 3 ++ .../lucene/facet/StringValueFacetCounts.java | 12 ++++-- .../facet/TestStringValueFacetCounts.java | 37 +++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index baa53ed5d90..3053955144d 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -283,6 +283,9 @@ Bug Fixes * GITHUB#13463: Address bug in MultiLeafKnnCollector causing #minCompetitiveSimilarity to stay artificially low in some corner cases. (Greg Miller) +* GITHUB#13493: StringValueFacetCunts stops throwing NPE when faceting over an empty match-set. (Grebennikov Roman, + Stefan Vodita) + Other -------------------- (No changes) diff --git a/lucene/facet/src/java/org/apache/lucene/facet/StringValueFacetCounts.java b/lucene/facet/src/java/org/apache/lucene/facet/StringValueFacetCounts.java index 67ce953067f..9e63043f3fa 100644 --- a/lucene/facet/src/java/org/apache/lucene/facet/StringValueFacetCounts.java +++ b/lucene/facet/src/java/org/apache/lucene/facet/StringValueFacetCounts.java @@ -154,7 +154,7 @@ public class StringValueFacetCounts extends Facets { final BytesRef term = docValues.lookupOrd(sparseCount.key); labelValues.add(new LabelAndValue(term.utf8ToString(), count)); } - } else { + } else if (denseCounts != null) { for (int i = 0; i < denseCounts.length; i++) { int count = denseCounts[i]; if (count != 0) { @@ -206,7 +206,7 @@ public class StringValueFacetCounts extends Facets { } } } - } else { + } else if (denseCounts != null) { for (int i = 0; i < denseCounts.length; i++) { int count = denseCounts[i]; if (count != 0) { @@ -256,7 +256,13 @@ public class StringValueFacetCounts extends Facets { return -1; } - return sparseCounts != null ? sparseCounts.get(ord) : denseCounts[ord]; + if (sparseCounts != null) { + return sparseCounts.get(ord); + } + if (denseCounts != null) { + return denseCounts[ord]; + } + return 0; } @Override diff --git a/lucene/facet/src/test/org/apache/lucene/facet/TestStringValueFacetCounts.java b/lucene/facet/src/test/org/apache/lucene/facet/TestStringValueFacetCounts.java index 45e48c5abc7..5c9a1eea7b4 100644 --- a/lucene/facet/src/test/org/apache/lucene/facet/TestStringValueFacetCounts.java +++ b/lucene/facet/src/test/org/apache/lucene/facet/TestStringValueFacetCounts.java @@ -34,6 +34,7 @@ import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.Term; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.store.Directory; import org.apache.lucene.tests.index.RandomIndexWriter; import org.apache.lucene.tests.util.LuceneTestCase; @@ -80,6 +81,42 @@ public class TestStringValueFacetCounts extends FacetTestCase { IOUtils.close(searcher.getIndexReader(), dir); } + private void assertEmptyFacetResult(FacetResult result) { + assertEquals(0, result.path.length); + assertEquals(0, result.value); + assertEquals(0, result.childCount); + assertEquals(0, result.labelValues.length); + } + + public void testEmptyMatchset() throws Exception { + Directory dir = newDirectory(); + RandomIndexWriter writer = new RandomIndexWriter(random(), dir); + + Document doc = new Document(); + doc.add(new SortedSetDocValuesField("field", new BytesRef("foo"))); + writer.addDocument(doc); + + IndexSearcher searcher = newSearcher(writer.getReader()); + writer.close(); + + FacetsCollector facetsCollector = + searcher.search(new MatchNoDocsQuery(), new FacetsCollectorManager()); + StringDocValuesReaderState state = + new StringDocValuesReaderState(searcher.getIndexReader(), "field"); + + StringValueFacetCounts counts = new StringValueFacetCounts(state, facetsCollector); + + FacetResult top = counts.getTopChildren(10, "field"); + assertEmptyFacetResult(top); + + FacetResult all = counts.getAllChildren("field"); + assertEmptyFacetResult(all); + + assertEquals(0, counts.getSpecificValue("field", "foo")); + + IOUtils.close(searcher.getIndexReader(), dir); + } + // See: LUCENE-10070 public void testCountAll() throws Exception { From 06c4a4b9e0fec9614a2471873fd63589dabc7d90 Mon Sep 17 00:00:00 2001 From: Zack Kendall Date: Fri, 21 Jun 2024 02:57:59 -0700 Subject: [PATCH 12/42] Prevent DefaultPassageFormatter from taking shorter overlapping passages (#13384) --- lucene/CHANGES.txt | 2 ++ .../uhighlight/DefaultPassageFormatter.java | 7 +++--- .../TestDefaultPassageFormatter.java | 23 +++++++++++++++++++ .../uhighlight/TestUnifiedHighlighter.java | 2 +- .../TestUnifiedHighlighterStrictPhrases.java | 16 +++++++++---- 5 files changed, 42 insertions(+), 8 deletions(-) diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 3053955144d..b6a97acfcfc 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -280,6 +280,8 @@ Optimizations Bug Fixes --------------------- +* GITHUB#13384: Fix highlighter to use longer passages instead of shorter individual terms. (Zack Kendall) + * GITHUB#13463: Address bug in MultiLeafKnnCollector causing #minCompetitiveSimilarity to stay artificially low in some corner cases. (Greg Miller) diff --git a/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/DefaultPassageFormatter.java b/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/DefaultPassageFormatter.java index b578c7427ce..27281a91be7 100644 --- a/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/DefaultPassageFormatter.java +++ b/lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/DefaultPassageFormatter.java @@ -76,10 +76,11 @@ public class DefaultPassageFormatter extends PassageFormatter { int end = passage.getMatchEnds()[i]; assert end > start; - // its possible to have overlapping terms. - // Look ahead to expand 'end' past all overlapping: + // It's possible to have overlapping terms. + // Look ahead to expand 'end' past all overlapping. + // Only take new end if it is larger than current end. while (i + 1 < passage.getNumMatches() && passage.getMatchStarts()[i + 1] < end) { - end = passage.getMatchEnds()[++i]; + end = Math.max(end, passage.getMatchEnds()[++i]); } end = Math.min(end, passage.getEndOffset()); // in case match straddles past passage diff --git a/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestDefaultPassageFormatter.java b/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestDefaultPassageFormatter.java index cfb9614b1a5..b59fea47453 100644 --- a/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestDefaultPassageFormatter.java +++ b/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestDefaultPassageFormatter.java @@ -18,6 +18,7 @@ package org.apache.lucene.search.uhighlight; import org.apache.lucene.tests.util.LuceneTestCase; +import org.apache.lucene.util.BytesRef; public class TestDefaultPassageFormatter extends LuceneTestCase { public void testBasic() throws Exception { @@ -52,4 +53,26 @@ public class TestDefaultPassageFormatter extends LuceneTestCase { + "</div> of this very formatter.\u2026 It's not very N/A!", formatter.format(passages, text)); } + + public void testOverlappingPassages() throws Exception { + String content = "Yin yang loooooooooong, yin gap yang yong"; + Passage[] passages = new Passage[1]; + passages[0] = new Passage(); + passages[0].setStartOffset(0); + passages[0].setEndOffset(41); + passages[0].setScore(5.93812f); + passages[0].setScore(5.93812f); + passages[0].addMatch(0, 3, new BytesRef("yin"), 1); + passages[0].addMatch(0, 22, new BytesRef("yin yang loooooooooooong"), 1); + passages[0].addMatch(4, 8, new BytesRef("yang"), 1); + passages[0].addMatch(9, 22, new BytesRef("loooooooooong"), 1); + passages[0].addMatch(24, 27, new BytesRef("yin"), 1); + passages[0].addMatch(32, 36, new BytesRef("yang"), 1); + + // test default + DefaultPassageFormatter formatter = new DefaultPassageFormatter(); + assertEquals( + "Yin yang loooooooooong, yin gap yang yong", + formatter.format(passages, content)); + } } diff --git a/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestUnifiedHighlighter.java b/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestUnifiedHighlighter.java index 01d5420522f..18720a3d0f4 100644 --- a/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestUnifiedHighlighter.java +++ b/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestUnifiedHighlighter.java @@ -1422,7 +1422,7 @@ public class TestUnifiedHighlighter extends UnifiedHighlighterTestBase { Set.of("field_tripples", "field_characters"), "danc", "dance with star", - "dance with star"); + "dance with star"); } } } diff --git a/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestUnifiedHighlighterStrictPhrases.java b/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestUnifiedHighlighterStrictPhrases.java index c299c63e57d..2b553d9af48 100644 --- a/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestUnifiedHighlighterStrictPhrases.java +++ b/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/TestUnifiedHighlighterStrictPhrases.java @@ -183,13 +183,15 @@ public class TestUnifiedHighlighterStrictPhrases extends UnifiedHighlighterTestB } public void testWithSameTermQuery() throws IOException { - indexWriter.addDocument(newDoc("Yin yang, yin gap yang")); + indexWriter.addDocument(newDoc("Yin yang loooooooooong, yin gap yang yong")); initReaderSearcherHighlighter(); BooleanQuery query = new BooleanQuery.Builder() .add(new TermQuery(new Term("body", "yin")), BooleanClause.Occur.MUST) - .add(newPhraseQuery("body", "yin yang"), BooleanClause.Occur.MUST) + .add(new TermQuery(new Term("body", "yang")), BooleanClause.Occur.MUST) + .add(new TermQuery(new Term("body", "loooooooooong")), BooleanClause.Occur.MUST) + .add(newPhraseQuery("body", "yin\\ yang\\ loooooooooong"), BooleanClause.Occur.MUST) // add queries for other fields; we shouldn't highlight these because of that. .add(new TermQuery(new Term("title", "yang")), BooleanClause.Occur.SHOULD) .build(); @@ -199,9 +201,15 @@ public class TestUnifiedHighlighterStrictPhrases extends UnifiedHighlighterTestB false); // We don't want duplicates from "Yin" being in TermQuery & PhraseQuery. String[] snippets = highlighter.highlight("body", query, topDocs); if (highlighter.getFlags("body").contains(HighlightFlag.WEIGHT_MATCHES)) { - assertArrayEquals(new String[] {"Yin yang, yin gap yang"}, snippets); + assertArrayEquals( + new String[] {"Yin yang loooooooooong, yin gap yang yong"}, + snippets); } else { - assertArrayEquals(new String[] {"Yin yang, yin gap yang"}, snippets); + assertArrayEquals( + new String[] { + "Yin yang loooooooooong, yin gap yang yong" + }, + snippets); } } From cfbf8d93df814939cfaf7feb28a76ce48dfd2009 Mon Sep 17 00:00:00 2001 From: Adrien Grand Date: Fri, 21 Jun 2024 14:39:41 +0200 Subject: [PATCH 13/42] Honor read advice on compound files. (#13467) This includes the following changes: - New `IndexInput#slice(String, long, long, ReadAdvice)` API that allows creating slices with different advices. - `PosixNativeAccess` now explicitly sets `MADV_NORMAL` when called with `ReadAdvice.NORMAL`. This is required to be able to override a `RANDOM` advice of a compound file with a `NORMAL` advice of a sub file of this compound file. - `PosixNativeAccess` now only ignores the first page if a range of bytes starts before the `MemorySegment` instead of the whole range. --- .../org/apache/lucene/store/IndexInput.java | 14 +++++ .../lucene/store/MemorySegmentIndexInput.java | 60 +++++++++++++++---- .../lucene/store/PosixNativeAccess.java | 4 +- 3 files changed, 66 insertions(+), 12 deletions(-) diff --git a/lucene/core/src/java/org/apache/lucene/store/IndexInput.java b/lucene/core/src/java/org/apache/lucene/store/IndexInput.java index 881f707d07a..ee84d908838 100644 --- a/lucene/core/src/java/org/apache/lucene/store/IndexInput.java +++ b/lucene/core/src/java/org/apache/lucene/store/IndexInput.java @@ -18,6 +18,7 @@ package org.apache.lucene.store; import java.io.Closeable; import java.io.IOException; +import org.apache.lucene.codecs.CompoundFormat; /** * Abstract base class for input from a file in a {@link Directory}. A random-access input stream. @@ -121,6 +122,19 @@ public abstract class IndexInput extends DataInput implements Closeable { public abstract IndexInput slice(String sliceDescription, long offset, long length) throws IOException; + /** + * Create a slice with a specific {@link ReadAdvice}. This is typically used by {@link + * CompoundFormat} implementations to honor the {@link ReadAdvice} of each file within the + * compound file. + * + *

The default implementation delegates to {@link #slice(String, long, long)} and ignores the + * {@link ReadAdvice}. + */ + public IndexInput slice(String sliceDescription, long offset, long length, ReadAdvice readAdvice) + throws IOException { + return slice(sliceDescription, offset, length); + } + /** * Subclasses call this to get the String for resourceDescription of a slice of this {@code * IndexInput}. diff --git a/lucene/core/src/java21/org/apache/lucene/store/MemorySegmentIndexInput.java b/lucene/core/src/java21/org/apache/lucene/store/MemorySegmentIndexInput.java index b689088c304..68f1e771195 100644 --- a/lucene/core/src/java21/org/apache/lucene/store/MemorySegmentIndexInput.java +++ b/lucene/core/src/java21/org/apache/lucene/store/MemorySegmentIndexInput.java @@ -28,6 +28,7 @@ import java.util.Optional; import org.apache.lucene.util.ArrayUtil; import org.apache.lucene.util.BitUtil; import org.apache.lucene.util.GroupVIntUtil; +import org.apache.lucene.util.IOConsumer; /** * Base IndexInput implementation that uses an array of MemorySegments to represent a file. @@ -333,14 +334,36 @@ abstract class MemorySegmentIndexInput extends IndexInput return; } + final NativeAccess nativeAccess = NATIVE_ACCESS.get(); + advise( + offset, + length, + segment -> { + if (segment.isLoaded() == false) { + // We have a cache miss on at least one page, let's reset the counter. + consecutivePrefetchHitCount = 0; + nativeAccess.madviseWillNeed(segment); + } + }); + } + + void advise(long offset, long length, IOConsumer advice) throws IOException { + if (NATIVE_ACCESS.isEmpty()) { + return; + } + + ensureOpen(); + + Objects.checkFromIndexSize(offset, length, length()); + final NativeAccess nativeAccess = NATIVE_ACCESS.get(); try { final MemorySegment segment = segments[(int) (offset >> chunkSizePower)]; offset &= chunkSizeMask; - // Compute the intersection of the current segment and the region that should be prefetched. + // Compute the intersection of the current segment and the region that should be advised. if (offset + length > segment.byteSize()) { - // Only prefetch bytes that are stored in the current segment. There may be bytes on the + // Only advise bytes that are stored in the current segment. There may be bytes on the // next segment but this case is rare enough that we don't try to optimize it and keep // things simple instead. length = segment.byteSize() - offset; @@ -351,16 +374,17 @@ abstract class MemorySegmentIndexInput extends IndexInput offset -= offsetInPage; length += offsetInPage; if (offset < 0) { - // The start of the page is outside of this segment, ignore. - return; + // The start of the page is before the start of this segment, ignore the first page. + offset += nativeAccess.getPageSize(); + length -= nativeAccess.getPageSize(); + if (length <= 0) { + // This segment has no data beyond the first page. + return; + } } - final MemorySegment prefetchSlice = segment.asSlice(offset, length); - if (prefetchSlice.isLoaded() == false) { - // We have a cache miss on at least one page, let's reset the counter. - consecutivePrefetchHitCount = 0; - nativeAccess.madviseWillNeed(prefetchSlice); - } + final MemorySegment advisedSlice = segment.asSlice(offset, length); + advice.accept(advisedSlice); } catch ( @SuppressWarnings("unused") IndexOutOfBoundsException e) { @@ -527,6 +551,22 @@ abstract class MemorySegmentIndexInput extends IndexInput return buildSlice(sliceDescription, offset, length); } + @Override + public final MemorySegmentIndexInput slice( + String sliceDescription, long offset, long length, ReadAdvice advice) throws IOException { + MemorySegmentIndexInput slice = slice(sliceDescription, offset, length); + if (NATIVE_ACCESS.isPresent()) { + final NativeAccess nativeAccess = NATIVE_ACCESS.get(); + slice.advise( + 0, + slice.length, + segment -> { + nativeAccess.madvise(segment, advice); + }); + } + return slice; + } + /** Builds the actual sliced IndexInput (may apply extra offset in subclasses). * */ MemorySegmentIndexInput buildSlice(String sliceDescription, long offset, long length) { ensureOpen(); diff --git a/lucene/core/src/java21/org/apache/lucene/store/PosixNativeAccess.java b/lucene/core/src/java21/org/apache/lucene/store/PosixNativeAccess.java index 93caca788b1..80c1665cdd1 100644 --- a/lucene/core/src/java21/org/apache/lucene/store/PosixNativeAccess.java +++ b/lucene/core/src/java21/org/apache/lucene/store/PosixNativeAccess.java @@ -158,10 +158,10 @@ final class PosixNativeAccess extends NativeAccess { private Integer mapReadAdvice(ReadAdvice readAdvice) { return switch (readAdvice) { - case NORMAL -> null; + case NORMAL -> POSIX_MADV_NORMAL; case RANDOM -> POSIX_MADV_RANDOM; case SEQUENTIAL -> POSIX_MADV_SEQUENTIAL; - case RANDOM_PRELOAD -> null; + case RANDOM_PRELOAD -> POSIX_MADV_NORMAL; }; } From 3ae59a9809d9239593aa94dcc23f8ce382d59e60 Mon Sep 17 00:00:00 2001 From: zhouhui Date: Sat, 22 Jun 2024 01:47:55 +0800 Subject: [PATCH 14/42] Fix Method declared 'final' in 'final' class. (#13492) --- .../core/src/java/org/apache/lucene/index/Term.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lucene/core/src/java/org/apache/lucene/index/Term.java b/lucene/core/src/java/org/apache/lucene/index/Term.java index 173a78241b5..6baa34cdf8c 100644 --- a/lucene/core/src/java/org/apache/lucene/index/Term.java +++ b/lucene/core/src/java/org/apache/lucene/index/Term.java @@ -89,7 +89,7 @@ public final class Term implements Comparable, Accountable { * Returns the field of this term. The field indicates the part of a document which this term came * from. */ - public final String field() { + public String field() { return field; } @@ -97,7 +97,7 @@ public final class Term implements Comparable, Accountable { * Returns the text of this term. In the case of words, this is simply the text of the word. In * the case of dates and other types, this is an encoding of the object as a string. */ - public final String text() { + public String text() { return toString(bytes); } @@ -105,7 +105,7 @@ public final class Term implements Comparable, Accountable { * Returns human-readable form of the term text. If the term is not unicode, the raw bytes will be * printed instead. */ - public static final String toString(BytesRef termText) { + public static String toString(BytesRef termText) { // the term might not be text, but usually is. so we make a best effort CharsetDecoder decoder = StandardCharsets.UTF_8 @@ -124,7 +124,7 @@ public final class Term implements Comparable, Accountable { } /** Returns the bytes of this term, these should not be modified. */ - public final BytesRef bytes() { + public BytesRef bytes() { return bytes; } @@ -160,7 +160,7 @@ public final class Term implements Comparable, Accountable { *

The ordering of terms is first by field, then by text. */ @Override - public final int compareTo(Term other) { + public int compareTo(Term other) { if (field.equals(other.field)) { return bytes.compareTo(other.bytes); } else { @@ -181,7 +181,7 @@ public final class Term implements Comparable, Accountable { } @Override - public final String toString() { + public String toString() { return field + ":" + text(); } From 33a4c1d8ef999902dacedde9c7f04a3c7e2e78c9 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Tue, 25 Jun 2024 16:15:30 +0200 Subject: [PATCH 15/42] Reduce memory use of MinimizationOperations#minimize (#13511) It is relatively easy to consume a massive amount of memory for the minimize operation, with its lists of boxed Integer (even though these are mostly cached, it's still more than 4b per instance to store them instead of plain storage) and neverending duplicate+empty StateList instances. The boxed integer situation we can fix and probably speedup by using the hppc primitive collections. To fix the duplicate/empty StateList instances, we can use a constant. This requires some hacky forking on the write path but that's about it. This is partly motivated by ES users at times creating broken, very long prefix queries that can then eat up GBs of heap. With this change, the examples I've been looking at become about 6x cheaper heap wise, making it less likely that kind of mistakes impacts stability. --- .../automaton/MinimizationOperations.java | 77 +++++++++++-------- 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/lucene/core/src/test/org/apache/lucene/util/automaton/MinimizationOperations.java b/lucene/core/src/test/org/apache/lucene/util/automaton/MinimizationOperations.java index d45d6027555..820ae1881c6 100644 --- a/lucene/core/src/test/org/apache/lucene/util/automaton/MinimizationOperations.java +++ b/lucene/core/src/test/org/apache/lucene/util/automaton/MinimizationOperations.java @@ -29,10 +29,11 @@ package org.apache.lucene.util.automaton; -import java.util.ArrayList; import java.util.BitSet; -import java.util.HashSet; import java.util.LinkedList; +import org.apache.lucene.internal.hppc.IntArrayList; +import org.apache.lucene.internal.hppc.IntCursor; +import org.apache.lucene.internal.hppc.IntHashSet; /** * Operations for minimizing automata. @@ -75,13 +76,9 @@ public final class MinimizationOperations { final int[] sigma = a.getStartPoints(); final int sigmaLen = sigma.length, statesLen = a.getNumStates(); - @SuppressWarnings({"rawtypes", "unchecked"}) - final ArrayList[][] reverse = - (ArrayList[][]) new ArrayList[statesLen][sigmaLen]; - @SuppressWarnings({"rawtypes", "unchecked"}) - final HashSet[] partition = (HashSet[]) new HashSet[statesLen]; - @SuppressWarnings({"rawtypes", "unchecked"}) - final ArrayList[] splitblock = (ArrayList[]) new ArrayList[statesLen]; + final IntArrayList[][] reverse = new IntArrayList[statesLen][sigmaLen]; + final IntHashSet[] partition = new IntHashSet[statesLen]; + final IntArrayList[] splitblock = new IntArrayList[statesLen]; final int[] block = new int[statesLen]; final StateList[][] active = new StateList[statesLen][sigmaLen]; final StateListNode[][] active2 = new StateListNode[statesLen][sigmaLen]; @@ -91,10 +88,10 @@ public final class MinimizationOperations { refine = new BitSet(statesLen), refine2 = new BitSet(statesLen); for (int q = 0; q < statesLen; q++) { - splitblock[q] = new ArrayList<>(); - partition[q] = new HashSet<>(); + splitblock[q] = new IntArrayList(); + partition[q] = new IntHashSet(); for (int x = 0; x < sigmaLen; x++) { - active[q][x] = new StateList(); + active[q][x] = StateList.EMPTY; } } // find initial partition and reverse edges @@ -106,9 +103,9 @@ public final class MinimizationOperations { transition.source = q; transition.transitionUpto = -1; for (int x = 0; x < sigmaLen; x++) { - final ArrayList[] r = reverse[a.next(transition, sigma[x])]; + final IntArrayList[] r = reverse[a.next(transition, sigma[x])]; if (r[x] == null) { - r[x] = new ArrayList<>(); + r[x] = new IntArrayList(); } r[x].add(q); } @@ -116,9 +113,15 @@ public final class MinimizationOperations { // initialize active sets for (int j = 0; j <= 1; j++) { for (int x = 0; x < sigmaLen; x++) { - for (int q : partition[j]) { + for (IntCursor qCursor : partition[j]) { + int q = qCursor.value; if (reverse[q][x] != null) { - active2[q][x] = active[j][x].add(q); + StateList stateList = active[j][x]; + if (stateList == StateList.EMPTY) { + stateList = new StateList(); + active[j][x] = stateList; + } + active2[q][x] = stateList.add(q); } } } @@ -143,9 +146,10 @@ public final class MinimizationOperations { pending2.clear(x * statesLen + p); // find states that need to be split off their blocks for (StateListNode m = active[p][x].first; m != null; m = m.next) { - final ArrayList r = reverse[m.q][x]; + final IntArrayList r = reverse[m.q][x]; if (r != null) { - for (int i : r) { + for (IntCursor iCursor : r) { + final int i = iCursor.value; if (!split.get(i)) { split.set(i); final int j = block[i]; @@ -161,11 +165,12 @@ public final class MinimizationOperations { // refine blocks for (int j = refine.nextSetBit(0); j >= 0; j = refine.nextSetBit(j + 1)) { - final ArrayList sb = splitblock[j]; + final IntArrayList sb = splitblock[j]; if (sb.size() < partition[j].size()) { - final HashSet b1 = partition[j]; - final HashSet b2 = partition[k]; - for (int s : sb) { + final IntHashSet b1 = partition[j]; + final IntHashSet b2 = partition[k]; + for (IntCursor iCursor : sb) { + final int s = iCursor.value; b1.remove(s); b2.add(s); block[s] = k; @@ -173,7 +178,12 @@ public final class MinimizationOperations { final StateListNode sn = active2[s][c]; if (sn != null && sn.sl == active[j][c]) { sn.remove(); - active2[s][c] = active[k][c].add(s); + StateList stateList = active[k][c]; + if (stateList == StateList.EMPTY) { + stateList = new StateList(); + active[k][c] = stateList; + } + active2[s][c] = stateList.add(s); } } } @@ -191,7 +201,8 @@ public final class MinimizationOperations { k++; } refine2.clear(j); - for (int s : sb) { + for (IntCursor iCursor : sb) { + final int s = iCursor.value; split.clear(s); } sb.clear(); @@ -215,17 +226,11 @@ public final class MinimizationOperations { for (int n = 0; n < k; n++) { // System.out.println(" n=" + n); - boolean isInitial = false; - for (int q : partition[n]) { - if (q == 0) { - isInitial = true; - // System.out.println(" isInitial!"); - break; - } - } + boolean isInitial = partition[n].contains(0); int newState; if (isInitial) { + // System.out.println(" isInitial!"); newState = 0; } else { newState = result.createState(); @@ -233,7 +238,8 @@ public final class MinimizationOperations { // System.out.println(" newState=" + newState); - for (int q : partition[n]) { + for (IntCursor qCursor : partition[n]) { + int q = qCursor.value; stateMap[q] = newState; // System.out.println(" q=" + q + " isAccept?=" + a.isAccept(q)); result.setAccept(newState, a.isAccept(q)); @@ -268,11 +274,16 @@ public final class MinimizationOperations { static final class StateList { + // Empty list that should never be mutated, used as a memory saving optimization instead of null + // so we don't need to branch the read path in #minimize + static final StateList EMPTY = new StateList(); + int size; StateListNode first, last; StateListNode add(int q) { + assert this != EMPTY; return new StateListNode(q, this); } } From 126834c09e69f0f0a3cfa5b09195f3ebfb5c1c0f Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Thu, 27 Jun 2024 09:52:10 +0200 Subject: [PATCH 16/42] Reduce overhead for FSTs in FieldReader (#13524) We don't need to clone the index input we hold on to in OffHeapFSTStore since we only use it for slicing from known coordinates anyway. -> remove the cloning and add the infrastructure to initialize OffHeapFSTStore without seeking the input to the starting offset. --- .../lucene40/blocktree/FieldReader.java | 18 ++++------ .../lucene/codecs/memory/FSTTermsReader.java | 5 +-- .../codecs/uniformsplit/FSTDictionary.java | 13 ++++--- .../lucene90/blocktree/FieldReader.java | 9 ++--- .../java/org/apache/lucene/util/fst/FST.java | 11 +----- .../org/apache/lucene/util/fst/FSTStore.java | 34 ------------------- .../lucene/util/fst/OffHeapFSTStore.java | 25 +++++--------- .../lucene/util/fst/OnHeapFSTStore.java | 17 +++------- .../lucene/util/fst/Test2BFSTOffHeap.java | 15 ++++++-- .../search/suggest/document/NRTSuggester.java | 9 +++-- 10 files changed, 50 insertions(+), 106 deletions(-) delete mode 100644 lucene/core/src/java/org/apache/lucene/util/fst/FSTStore.java diff --git a/lucene/backward-codecs/src/java/org/apache/lucene/backward_codecs/lucene40/blocktree/FieldReader.java b/lucene/backward-codecs/src/java/org/apache/lucene/backward_codecs/lucene40/blocktree/FieldReader.java index 3387c2c7b5c..18ba067b2bb 100644 --- a/lucene/backward-codecs/src/java/org/apache/lucene/backward_codecs/lucene40/blocktree/FieldReader.java +++ b/lucene/backward-codecs/src/java/org/apache/lucene/backward_codecs/lucene40/blocktree/FieldReader.java @@ -88,21 +88,15 @@ public final class FieldReader extends Terms { (new ByteArrayDataInput(rootCode.bytes, rootCode.offset, rootCode.length)).readVLong() >>> Lucene40BlockTreeTermsReader.OUTPUT_FLAGS_NUM_BITS; // Initialize FST always off-heap. - final IndexInput clone = indexIn.clone(); - clone.seek(indexStartFP); + final FST.FSTMetadata fstMetadata; if (metaIn == indexIn) { // Only true before Lucene 8.6 - index = - new FST<>( - readMetadata(clone, ByteSequenceOutputs.getSingleton()), - clone, - new OffHeapFSTStore()); + final IndexInput clone = indexIn.clone(); + clone.seek(indexStartFP); + fstMetadata = readMetadata(clone, ByteSequenceOutputs.getSingleton()); } else { - index = - new FST<>( - readMetadata(metaIn, ByteSequenceOutputs.getSingleton()), - clone, - new OffHeapFSTStore()); + fstMetadata = readMetadata(metaIn, ByteSequenceOutputs.getSingleton()); } + index = FST.fromFSTReader(fstMetadata, new OffHeapFSTStore(indexIn, indexStartFP, fstMetadata)); /* if (false) { final String dotFileName = segment + "_" + fieldInfo.name + ".dot"; diff --git a/lucene/codecs/src/java/org/apache/lucene/codecs/memory/FSTTermsReader.java b/lucene/codecs/src/java/org/apache/lucene/codecs/memory/FSTTermsReader.java index 6f4e2db0d6e..d500b75eaab 100644 --- a/lucene/codecs/src/java/org/apache/lucene/codecs/memory/FSTTermsReader.java +++ b/lucene/codecs/src/java/org/apache/lucene/codecs/memory/FSTTermsReader.java @@ -195,9 +195,10 @@ public class FSTTermsReader extends FieldsProducer { this.sumTotalTermFreq = sumTotalTermFreq; this.sumDocFreq = sumDocFreq; this.docCount = docCount; - OffHeapFSTStore offHeapFSTStore = new OffHeapFSTStore(); FSTTermOutputs outputs = new FSTTermOutputs(fieldInfo); - this.dict = new FST<>(FST.readMetadata(in, outputs), in, offHeapFSTStore); + final var fstMetadata = FST.readMetadata(in, outputs); + OffHeapFSTStore offHeapFSTStore = new OffHeapFSTStore(in, in.getFilePointer(), fstMetadata); + this.dict = FST.fromFSTReader(fstMetadata, offHeapFSTStore); in.skipBytes(offHeapFSTStore.size()); } diff --git a/lucene/codecs/src/java/org/apache/lucene/codecs/uniformsplit/FSTDictionary.java b/lucene/codecs/src/java/org/apache/lucene/codecs/uniformsplit/FSTDictionary.java index a73fef410dd..ced1dadab6f 100644 --- a/lucene/codecs/src/java/org/apache/lucene/codecs/uniformsplit/FSTDictionary.java +++ b/lucene/codecs/src/java/org/apache/lucene/codecs/uniformsplit/FSTDictionary.java @@ -90,10 +90,15 @@ public class FSTDictionary implements IndexDictionary { } PositiveIntOutputs fstOutputs = PositiveIntOutputs.getSingleton(); FST.FSTMetadata metadata = FST.readMetadata(fstDataInput, fstOutputs); - FST fst = - isFSTOnHeap - ? new FST<>(metadata, fstDataInput) - : new FST<>(metadata, fstDataInput, new OffHeapFSTStore()); + FST fst; + if (isFSTOnHeap) { + fst = new FST<>(metadata, fstDataInput); + } else { + final IndexInput indexInput = (IndexInput) fstDataInput; + fst = + FST.fromFSTReader( + metadata, new OffHeapFSTStore(indexInput, indexInput.getFilePointer(), metadata)); + } return new FSTDictionary(fst); } diff --git a/lucene/core/src/java/org/apache/lucene/codecs/lucene90/blocktree/FieldReader.java b/lucene/core/src/java/org/apache/lucene/codecs/lucene90/blocktree/FieldReader.java index 6cef964a6a5..ed3e827c37c 100644 --- a/lucene/core/src/java/org/apache/lucene/codecs/lucene90/blocktree/FieldReader.java +++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene90/blocktree/FieldReader.java @@ -89,13 +89,8 @@ public final class FieldReader extends Terms { readVLongOutput(new ByteArrayDataInput(rootCode.bytes, rootCode.offset, rootCode.length)) >>> Lucene90BlockTreeTermsReader.OUTPUT_FLAGS_NUM_BITS; // Initialize FST always off-heap. - final IndexInput clone = indexIn.clone(); - clone.seek(indexStartFP); - index = - new FST<>( - FST.readMetadata(metaIn, ByteSequenceOutputs.getSingleton()), - clone, - new OffHeapFSTStore()); + var metadata = FST.readMetadata(metaIn, ByteSequenceOutputs.getSingleton()); + index = FST.fromFSTReader(metadata, new OffHeapFSTStore(indexIn, indexStartFP, metadata)); /* if (false) { final String dotFileName = segment + "_" + fieldInfo.name + ".dot"; diff --git a/lucene/core/src/java/org/apache/lucene/util/fst/FST.java b/lucene/core/src/java/org/apache/lucene/util/fst/FST.java index 6bb5718d5c7..17201194da4 100644 --- a/lucene/core/src/java/org/apache/lucene/util/fst/FST.java +++ b/lucene/core/src/java/org/apache/lucene/util/fst/FST.java @@ -417,16 +417,7 @@ public final class FST implements Accountable { * maxBlockBits set to {@link #DEFAULT_MAX_BLOCK_BITS} */ public FST(FSTMetadata metadata, DataInput in) throws IOException { - this(metadata, in, new OnHeapFSTStore(DEFAULT_MAX_BLOCK_BITS)); - } - - /** - * Load a previously saved FST with a metdata object and a FSTStore. If using {@link - * OnHeapFSTStore}, setting maxBlockBits allows you to control the size of the byte[] pages used - * to hold the FST bytes. - */ - public FST(FSTMetadata metadata, DataInput in, FSTStore fstStore) throws IOException { - this(metadata, fstStore.init(in, metadata.numBytes)); + this(metadata, new OnHeapFSTStore(DEFAULT_MAX_BLOCK_BITS, in, metadata.numBytes)); } /** Create the FST with a metadata object and a FSTReader. */ diff --git a/lucene/core/src/java/org/apache/lucene/util/fst/FSTStore.java b/lucene/core/src/java/org/apache/lucene/util/fst/FSTStore.java deleted file mode 100644 index 682503b2c44..00000000000 --- a/lucene/core/src/java/org/apache/lucene/util/fst/FSTStore.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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.util.fst; - -import java.io.IOException; -import org.apache.lucene.store.DataInput; - -/** A type of {@link FSTReader} which needs data to be initialized before use */ -public interface FSTStore extends FSTReader { - - /** - * Initialize the FSTStore - * - * @param in the DataInput to read from - * @param numBytes the number of bytes to read - * @return this FSTStore - * @throws IOException if exception occurred during reading the DataInput - */ - FSTStore init(DataInput in, long numBytes) throws IOException; -} diff --git a/lucene/core/src/java/org/apache/lucene/util/fst/OffHeapFSTStore.java b/lucene/core/src/java/org/apache/lucene/util/fst/OffHeapFSTStore.java index f88715b191c..e8b00037d15 100644 --- a/lucene/core/src/java/org/apache/lucene/util/fst/OffHeapFSTStore.java +++ b/lucene/core/src/java/org/apache/lucene/util/fst/OffHeapFSTStore.java @@ -17,7 +17,6 @@ package org.apache.lucene.util.fst; import java.io.IOException; -import org.apache.lucene.store.DataInput; import org.apache.lucene.store.DataOutput; import org.apache.lucene.store.IndexInput; import org.apache.lucene.util.RamUsageEstimator; @@ -28,27 +27,19 @@ import org.apache.lucene.util.RamUsageEstimator; * * @lucene.experimental */ -public final class OffHeapFSTStore implements FSTStore { +public final class OffHeapFSTStore implements FSTReader { private static final long BASE_RAM_BYTES_USED = RamUsageEstimator.shallowSizeOfInstance(OffHeapFSTStore.class); - private IndexInput in; - private long offset; - private long numBytes; + private final IndexInput in; + private final long offset; + private final long numBytes; - @Override - public FSTStore init(DataInput in, long numBytes) throws IOException { - if (in instanceof IndexInput) { - this.in = (IndexInput) in; - this.numBytes = numBytes; - this.offset = this.in.getFilePointer(); - } else { - throw new IllegalArgumentException( - "parameter:in should be an instance of IndexInput for using OffHeapFSTStore, not a " - + in.getClass().getName()); - } - return this; + public OffHeapFSTStore(IndexInput in, long offset, FST.FSTMetadata metadata) { + this.in = in; + this.offset = offset; + this.numBytes = metadata.numBytes; } @Override diff --git a/lucene/core/src/java/org/apache/lucene/util/fst/OnHeapFSTStore.java b/lucene/core/src/java/org/apache/lucene/util/fst/OnHeapFSTStore.java index 949babdaa88..15bb0bb5f8a 100644 --- a/lucene/core/src/java/org/apache/lucene/util/fst/OnHeapFSTStore.java +++ b/lucene/core/src/java/org/apache/lucene/util/fst/OnHeapFSTStore.java @@ -28,7 +28,7 @@ import org.apache.lucene.util.RamUsageEstimator; * * @lucene.experimental */ -public final class OnHeapFSTStore implements FSTStore { +public final class OnHeapFSTStore implements FSTReader { private static final long BASE_RAM_BYTES_USED = RamUsageEstimator.shallowSizeOfInstance(OnHeapFSTStore.class); @@ -40,31 +40,24 @@ public final class OnHeapFSTStore implements FSTStore { private ReadWriteDataOutput dataOutput; /** Used at read time when the FST fits into a single byte[]. */ - private byte[] bytesArray; + private final byte[] bytesArray; - private final int maxBlockBits; - - public OnHeapFSTStore(int maxBlockBits) { + public OnHeapFSTStore(int maxBlockBits, DataInput in, long numBytes) throws IOException { if (maxBlockBits < 1 || maxBlockBits > 30) { throw new IllegalArgumentException("maxBlockBits should be 1 .. 30; got " + maxBlockBits); } - this.maxBlockBits = maxBlockBits; - } - - @Override - public FSTStore init(DataInput in, long numBytes) throws IOException { - if (numBytes > 1 << this.maxBlockBits) { + if (numBytes > 1 << maxBlockBits) { // FST is big: we need multiple pages dataOutput = (ReadWriteDataOutput) getOnHeapReaderWriter(maxBlockBits); dataOutput.copyBytes(in, numBytes); dataOutput.freeze(); + bytesArray = null; } else { // FST fits into a single block: use ByteArrayBytesStoreReader for less overhead bytesArray = new byte[(int) numBytes]; in.readBytes(bytesArray, 0, bytesArray.length); } - return this; } @Override diff --git a/lucene/core/src/test/org/apache/lucene/util/fst/Test2BFSTOffHeap.java b/lucene/core/src/test/org/apache/lucene/util/fst/Test2BFSTOffHeap.java index 42fd4e1128a..2169c093fca 100644 --- a/lucene/core/src/test/org/apache/lucene/util/fst/Test2BFSTOffHeap.java +++ b/lucene/core/src/test/org/apache/lucene/util/fst/Test2BFSTOffHeap.java @@ -93,7 +93,10 @@ public class Test2BFSTOffHeap extends LuceneTestCase { FST.FSTMetadata fstMetadata = fstCompiler.compile(); indexOutput.close(); try (IndexInput indexInput = dir.openInput("fst", IOContext.DEFAULT)) { - FST fst = new FST<>(fstMetadata, indexInput, new OffHeapFSTStore()); + FST fst = + FST.fromFSTReader( + fstMetadata, + new OffHeapFSTStore(indexInput, indexInput.getFilePointer(), fstMetadata)); for (int verify = 0; verify < 2; verify++) { System.out.println( @@ -181,7 +184,10 @@ public class Test2BFSTOffHeap extends LuceneTestCase { FST.FSTMetadata fstMetadata = fstCompiler.compile(); indexOutput.close(); try (IndexInput indexInput = dir.openInput("fst", IOContext.DEFAULT)) { - FST fst = new FST<>(fstMetadata, indexInput, new OffHeapFSTStore()); + FST fst = + FST.fromFSTReader( + fstMetadata, + new OffHeapFSTStore(indexInput, indexInput.getFilePointer(), fstMetadata)); for (int verify = 0; verify < 2; verify++) { System.out.println( @@ -266,7 +272,10 @@ public class Test2BFSTOffHeap extends LuceneTestCase { FST.FSTMetadata fstMetadata = fstCompiler.compile(); indexOutput.close(); try (IndexInput indexInput = dir.openInput("fst", IOContext.DEFAULT)) { - FST fst = new FST<>(fstMetadata, indexInput, new OffHeapFSTStore()); + FST fst = + FST.fromFSTReader( + fstMetadata, + new OffHeapFSTStore(indexInput, indexInput.getFilePointer(), fstMetadata)); for (int verify = 0; verify < 2; verify++) { diff --git a/lucene/suggest/src/java/org/apache/lucene/search/suggest/document/NRTSuggester.java b/lucene/suggest/src/java/org/apache/lucene/search/suggest/document/NRTSuggester.java index b059d3e7356..a56365fed9d 100644 --- a/lucene/suggest/src/java/org/apache/lucene/search/suggest/document/NRTSuggester.java +++ b/lucene/suggest/src/java/org/apache/lucene/search/suggest/document/NRTSuggester.java @@ -341,11 +341,10 @@ public final class NRTSuggester implements Accountable { PairOutputs outputs = new PairOutputs<>(PositiveIntOutputs.getSingleton(), ByteSequenceOutputs.getSingleton()); if (shouldLoadFSTOffHeap(input, fstLoadMode)) { - OffHeapFSTStore store = new OffHeapFSTStore(); - IndexInput clone = input.clone(); - clone.seek(input.getFilePointer()); - fst = new FST<>(FST.readMetadata(clone, outputs), clone, store); - input.seek(clone.getFilePointer() + store.size()); + final FST.FSTMetadata> fstMetadata = FST.readMetadata(input, outputs); + OffHeapFSTStore store = new OffHeapFSTStore(input, input.getFilePointer(), fstMetadata); + fst = FST.fromFSTReader(fstMetadata, store); + input.skipBytes(store.size()); } else { fst = new FST<>(FST.readMetadata(input, outputs), input); } From b737c5f7074cd7a978e3a2c8de8e0683a40d9c64 Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Thu, 27 Jun 2024 15:40:55 +0200 Subject: [PATCH 17/42] DOAP changes for release 9.11.1 --- dev-tools/doap/lucene.rdf | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dev-tools/doap/lucene.rdf b/dev-tools/doap/lucene.rdf index b91eec32904..7c400eb545b 100644 --- a/dev-tools/doap/lucene.rdf +++ b/dev-tools/doap/lucene.rdf @@ -67,6 +67,13 @@ + + + lucene-9.11.1 + 2024-06-27 + 9.11.1 + + . lucene-9.11.0 From 803a743cd7bcc617acba1007e5b7f162392a2686 Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Thu, 27 Jun 2024 16:02:56 +0200 Subject: [PATCH 18/42] Add bugfix version 9.11.1 --- lucene/CHANGES.txt | 6 ++++++ lucene/core/src/java/org/apache/lucene/util/Version.java | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index b6a97acfcfc..ea06a241ee6 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -292,6 +292,12 @@ Other -------------------- (No changes) +======================== Lucene 9.11.1 ======================= + +Bug Fixes +--------------------- +(No changes) + ======================== Lucene 9.11.0 ======================= API Changes diff --git a/lucene/core/src/java/org/apache/lucene/util/Version.java b/lucene/core/src/java/org/apache/lucene/util/Version.java index c83222cebf2..604735ea710 100644 --- a/lucene/core/src/java/org/apache/lucene/util/Version.java +++ b/lucene/core/src/java/org/apache/lucene/util/Version.java @@ -142,9 +142,17 @@ public final class Version { * * @deprecated Use latest * @deprecated (9.12.0) Use latest + * @deprecated (9.11.1) Use latest */ @Deprecated public static final Version LUCENE_9_11_0 = new Version(9, 11, 0); + /** + * Match settings and bugs in Lucene's 9.11.1 release. + * @deprecated Use latest + */ + @Deprecated + public static final Version LUCENE_9_11_1 = new Version(9, 11, 1); + /** * Match settings and bugs in Lucene's 9.12.0 release. * From d47a5afc6586660241c513f147ef7f7a9e92fe4b Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Thu, 27 Jun 2024 16:08:48 +0200 Subject: [PATCH 19/42] spotless --- lucene/core/src/java/org/apache/lucene/util/Version.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lucene/core/src/java/org/apache/lucene/util/Version.java b/lucene/core/src/java/org/apache/lucene/util/Version.java index 604735ea710..91eb4649efc 100644 --- a/lucene/core/src/java/org/apache/lucene/util/Version.java +++ b/lucene/core/src/java/org/apache/lucene/util/Version.java @@ -148,10 +148,10 @@ public final class Version { /** * Match settings and bugs in Lucene's 9.11.1 release. + * * @deprecated Use latest */ - @Deprecated - public static final Version LUCENE_9_11_1 = new Version(9, 11, 1); + @Deprecated public static final Version LUCENE_9_11_1 = new Version(9, 11, 1); /** * Match settings and bugs in Lucene's 9.12.0 release. From 2aec233b5c5cce9ff4d4a43f76842590b1935dd3 Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Thu, 27 Jun 2024 16:14:36 +0200 Subject: [PATCH 20/42] Sync CHANGES for 9.11.1 --- lucene/CHANGES.txt | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index ea06a241ee6..320de7aaefe 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -285,9 +285,6 @@ Bug Fixes * GITHUB#13463: Address bug in MultiLeafKnnCollector causing #minCompetitiveSimilarity to stay artificially low in some corner cases. (Greg Miller) -* GITHUB#13493: StringValueFacetCunts stops throwing NPE when faceting over an empty match-set. (Grebennikov Roman, - Stefan Vodita) - Other -------------------- (No changes) @@ -296,7 +293,19 @@ Other Bug Fixes --------------------- -(No changes) + +* GITHUB#13498: Avoid performance regression by constructing lazily the PointTree in NumericComparator. (Ignacio Vera) + +* GITHUB#13501, GITHUB#13478: Remove intra-merge parallelism for everything except HNSW graph merges. (Ben Trent) + +* GITHUB#13498, GITHUB#13340: Allow adding a parent field to an index with no fields (Michael Sokolov) + +* GITHUB#12431: Fix IndexOutOfBoundsException thrown in DefaultPassageFormatter + by unordered matches. (Stephane Campinas) + +* GITHUB#13493: StringValueFacetCounts stops throwing NPE when faceting over an empty match-set. (Grebennikov Roman, + Stefan Vodita) + ======================== Lucene 9.11.0 ======================= From f8ee339f64c809a29f0be17e87ce31a92b188fc9 Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Thu, 27 Jun 2024 16:23:03 +0200 Subject: [PATCH 21/42] Add back-compat indices for 9.11.1 --- .../lucene/backward_index/index.9.11.1-cfs.zip | Bin 0 -> 17132 bytes .../backward_index/index.9.11.1-nocfs.zip | Bin 0 -> 17146 bytes .../lucene/backward_index/int8_hnsw.9.11.1.zip | Bin 0 -> 4795 bytes .../lucene/backward_index/sorted.9.11.1.zip | Bin 0 -> 136964 bytes .../apache/lucene/backward_index/versions.txt | 1 + 5 files changed, 1 insertion(+) create mode 100644 lucene/backward-codecs/src/test/org/apache/lucene/backward_index/index.9.11.1-cfs.zip create mode 100644 lucene/backward-codecs/src/test/org/apache/lucene/backward_index/index.9.11.1-nocfs.zip create mode 100644 lucene/backward-codecs/src/test/org/apache/lucene/backward_index/int8_hnsw.9.11.1.zip create mode 100644 lucene/backward-codecs/src/test/org/apache/lucene/backward_index/sorted.9.11.1.zip diff --git a/lucene/backward-codecs/src/test/org/apache/lucene/backward_index/index.9.11.1-cfs.zip b/lucene/backward-codecs/src/test/org/apache/lucene/backward_index/index.9.11.1-cfs.zip new file mode 100644 index 0000000000000000000000000000000000000000..51ad7e7997c30e6506e4e739af5486ff006812b8 GIT binary patch literal 17132 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 S9ti*mpz7n>7AW|BDgQ6x|1Nj{ literal 0 HcmV?d00001 diff --git a/lucene/backward-codecs/src/test/org/apache/lucene/backward_index/index.9.11.1-nocfs.zip b/lucene/backward-codecs/src/test/org/apache/lucene/backward_index/index.9.11.1-nocfs.zip new file mode 100644 index 0000000000000000000000000000000000000000..f14d925827ba7a2fca19cbae5c5548b0ea62470e GIT binary patch literal 17146 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 U{<0nk014pR`&Tcp@%>c(KfN_;cmMzZ literal 0 HcmV?d00001 diff --git a/lucene/backward-codecs/src/test/org/apache/lucene/backward_index/int8_hnsw.9.11.1.zip b/lucene/backward-codecs/src/test/org/apache/lucene/backward_index/int8_hnsw.9.11.1.zip new file mode 100644 index 0000000000000000000000000000000000000000..c4bb86b5f1b6e62807500227426307e558142cd7 GIT binary patch literal 4795 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

Wr{L;>3o2-2q9!2(~Bh} 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 literal 0 HcmV?d00001 diff --git a/lucene/backward-codecs/src/test/org/apache/lucene/backward_index/versions.txt b/lucene/backward-codecs/src/test/org/apache/lucene/backward_index/versions.txt index 3b820b6fc0f..4572b6fadfe 100644 --- a/lucene/backward-codecs/src/test/org/apache/lucene/backward_index/versions.txt +++ b/lucene/backward-codecs/src/test/org/apache/lucene/backward_index/versions.txt @@ -40,3 +40,4 @@ 9.9.2 9.10.0 9.11.0 +9.11.1 From 44ad4d95c6226e1a07f9b364ae415623247295e9 Mon Sep 17 00:00:00 2001 From: Adrien Grand Date: Fri, 28 Jun 2024 21:29:00 +0200 Subject: [PATCH 22/42] Add bw tests for block-tree with inlined metadata. (#13527) The backport of #13524 found a hole in the testing of `Lucene40BlockTreeTerms` for versions before we moved metadata to its own file. This PR adds explicit bw testing for this version. Adding the correct if/else statements made the code extremely complicated so I opted for restoring the file as it was at the time when we bumped the version. This also fixes the bug that we introduced in #13524. --- .../lucene40/blocktree/FieldReader.java | 2 + .../Lucene40BlockTreeTermsWriterV5.java | 1096 +++++++++++++++++ .../lucene50/Lucene50RWPostingsFormat.java | 8 +- .../TestAncientIndicesCompatibility.java | 1 + 4 files changed, 1103 insertions(+), 4 deletions(-) create mode 100644 lucene/backward-codecs/src/test/org/apache/lucene/backward_codecs/lucene40/blocktree/Lucene40BlockTreeTermsWriterV5.java diff --git a/lucene/backward-codecs/src/java/org/apache/lucene/backward_codecs/lucene40/blocktree/FieldReader.java b/lucene/backward-codecs/src/java/org/apache/lucene/backward_codecs/lucene40/blocktree/FieldReader.java index 18ba067b2bb..05b350d5ba2 100644 --- a/lucene/backward-codecs/src/java/org/apache/lucene/backward_codecs/lucene40/blocktree/FieldReader.java +++ b/lucene/backward-codecs/src/java/org/apache/lucene/backward_codecs/lucene40/blocktree/FieldReader.java @@ -93,6 +93,8 @@ public final class FieldReader extends Terms { final IndexInput clone = indexIn.clone(); clone.seek(indexStartFP); fstMetadata = readMetadata(clone, ByteSequenceOutputs.getSingleton()); + // FST bytes actually only start after the metadata. + indexStartFP = clone.getFilePointer(); } else { fstMetadata = readMetadata(metaIn, ByteSequenceOutputs.getSingleton()); } diff --git a/lucene/backward-codecs/src/test/org/apache/lucene/backward_codecs/lucene40/blocktree/Lucene40BlockTreeTermsWriterV5.java b/lucene/backward-codecs/src/test/org/apache/lucene/backward_codecs/lucene40/blocktree/Lucene40BlockTreeTermsWriterV5.java new file mode 100644 index 00000000000..165f296f282 --- /dev/null +++ b/lucene/backward-codecs/src/test/org/apache/lucene/backward_codecs/lucene40/blocktree/Lucene40BlockTreeTermsWriterV5.java @@ -0,0 +1,1096 @@ +/* + * 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.backward_codecs.lucene40.blocktree; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import org.apache.lucene.backward_codecs.store.EndiannessReverserUtil; +import org.apache.lucene.codecs.BlockTermState; +import org.apache.lucene.codecs.CodecUtil; +import org.apache.lucene.codecs.FieldsConsumer; +import org.apache.lucene.codecs.NormsProducer; +import org.apache.lucene.codecs.PostingsWriterBase; +import org.apache.lucene.index.FieldInfo; +import org.apache.lucene.index.FieldInfos; +import org.apache.lucene.index.Fields; +import org.apache.lucene.index.IndexFileNames; +import org.apache.lucene.index.IndexOptions; +import org.apache.lucene.index.SegmentWriteState; +import org.apache.lucene.index.Terms; +import org.apache.lucene.index.TermsEnum; +import org.apache.lucene.store.ByteArrayDataOutput; +import org.apache.lucene.store.ByteBuffersDataOutput; +import org.apache.lucene.store.DataOutput; +import org.apache.lucene.store.IndexOutput; +import org.apache.lucene.util.ArrayUtil; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.BytesRefBuilder; +import org.apache.lucene.util.FixedBitSet; +import org.apache.lucene.util.IOUtils; +import org.apache.lucene.util.IntsRefBuilder; +import org.apache.lucene.util.StringHelper; +import org.apache.lucene.util.compress.LZ4; +import org.apache.lucene.util.compress.LowercaseAsciiCompression; +import org.apache.lucene.util.fst.ByteSequenceOutputs; +import org.apache.lucene.util.fst.BytesRefFSTEnum; +import org.apache.lucene.util.fst.FST; +import org.apache.lucene.util.fst.FSTCompiler; +import org.apache.lucene.util.fst.Util; + +/** + * Writer for {@link Lucene40BlockTreeTermsReader} prior to the refactoring that separated metadata + * to its own file. + * + * @see Lucene40BlockTreeTermsReader#VERSION_META_FILE + */ +public final class Lucene40BlockTreeTermsWriterV5 extends FieldsConsumer { + + /** + * Suggested default value for the {@code minItemsInBlock} parameter to {@link + * #Lucene40BlockTreeTermsWriterV5(SegmentWriteState,PostingsWriterBase,int,int)}. + */ + public static final int DEFAULT_MIN_BLOCK_SIZE = 25; + + /** + * Suggested default value for the {@code maxItemsInBlock} parameter to {@link + * #Lucene40BlockTreeTermsWriterV5(SegmentWriteState,PostingsWriterBase,int,int)}. + */ + public static final int DEFAULT_MAX_BLOCK_SIZE = 48; + + private static final int VERSION_CURRENT = Lucene40BlockTreeTermsReader.VERSION_META_FILE - 1; + + // public static boolean DEBUG = false; + // public static boolean DEBUG2 = false; + + // private final static boolean SAVE_DOT_FILES = false; + + private final IndexOutput termsOut; + private final IndexOutput indexOut; + final int maxDoc; + final int minItemsInBlock; + final int maxItemsInBlock; + + final PostingsWriterBase postingsWriter; + final FieldInfos fieldInfos; + + private static class FieldMetaData { + public final FieldInfo fieldInfo; + public final BytesRef rootCode; + public final long numTerms; + public final long indexStartFP; + public final long sumTotalTermFreq; + public final long sumDocFreq; + public final int docCount; + public final BytesRef minTerm; + public final BytesRef maxTerm; + + public FieldMetaData( + FieldInfo fieldInfo, + BytesRef rootCode, + long numTerms, + long indexStartFP, + long sumTotalTermFreq, + long sumDocFreq, + int docCount, + BytesRef minTerm, + BytesRef maxTerm) { + assert numTerms > 0; + this.fieldInfo = fieldInfo; + assert rootCode != null : "field=" + fieldInfo.name + " numTerms=" + numTerms; + this.rootCode = rootCode; + this.indexStartFP = indexStartFP; + this.numTerms = numTerms; + this.sumTotalTermFreq = sumTotalTermFreq; + this.sumDocFreq = sumDocFreq; + this.docCount = docCount; + this.minTerm = minTerm; + this.maxTerm = maxTerm; + } + } + + private final List fields = new ArrayList<>(); + + /** + * Create a new writer. The number of items (terms or sub-blocks) per block will aim to be between + * minItemsPerBlock and maxItemsPerBlock, though in some cases the blocks may be smaller than the + * min. + */ + public Lucene40BlockTreeTermsWriterV5( + SegmentWriteState state, + PostingsWriterBase postingsWriter, + int minItemsInBlock, + int maxItemsInBlock) + throws IOException { + validateSettings(minItemsInBlock, maxItemsInBlock); + + this.minItemsInBlock = minItemsInBlock; + this.maxItemsInBlock = maxItemsInBlock; + + this.maxDoc = state.segmentInfo.maxDoc(); + this.fieldInfos = state.fieldInfos; + this.postingsWriter = postingsWriter; + + final String termsName = + IndexFileNames.segmentFileName( + state.segmentInfo.name, + state.segmentSuffix, + Lucene40BlockTreeTermsReader.TERMS_EXTENSION); + termsOut = EndiannessReverserUtil.createOutput(state.directory, termsName, state.context); + boolean success = false; + IndexOutput indexOut = null; + try { + CodecUtil.writeIndexHeader( + termsOut, + Lucene40BlockTreeTermsReader.TERMS_CODEC_NAME, + VERSION_CURRENT, + state.segmentInfo.getId(), + state.segmentSuffix); + + final String indexName = + IndexFileNames.segmentFileName( + state.segmentInfo.name, + state.segmentSuffix, + Lucene40BlockTreeTermsReader.TERMS_INDEX_EXTENSION); + indexOut = EndiannessReverserUtil.createOutput(state.directory, indexName, state.context); + CodecUtil.writeIndexHeader( + indexOut, + Lucene40BlockTreeTermsReader.TERMS_INDEX_CODEC_NAME, + VERSION_CURRENT, + state.segmentInfo.getId(), + state.segmentSuffix); + // segment = state.segmentInfo.name; + + postingsWriter.init(termsOut, state); // have consumer write its format/header + + this.indexOut = indexOut; + success = true; + } finally { + if (!success) { + IOUtils.closeWhileHandlingException(termsOut, indexOut); + } + } + } + + /** Writes the terms file trailer. */ + private void writeTrailer(IndexOutput out, long dirStart) throws IOException { + out.writeLong(dirStart); + } + + /** Writes the index file trailer. */ + private void writeIndexTrailer(IndexOutput indexOut, long dirStart) throws IOException { + indexOut.writeLong(dirStart); + } + + /** Throws {@code IllegalArgumentException} if any of these settings is invalid. */ + public static void validateSettings(int minItemsInBlock, int maxItemsInBlock) { + if (minItemsInBlock <= 1) { + throw new IllegalArgumentException("minItemsInBlock must be >= 2; got " + minItemsInBlock); + } + if (minItemsInBlock > maxItemsInBlock) { + throw new IllegalArgumentException( + "maxItemsInBlock must be >= minItemsInBlock; got maxItemsInBlock=" + + maxItemsInBlock + + " minItemsInBlock=" + + minItemsInBlock); + } + if (2 * (minItemsInBlock - 1) > maxItemsInBlock) { + throw new IllegalArgumentException( + "maxItemsInBlock must be at least 2*(minItemsInBlock-1); got maxItemsInBlock=" + + maxItemsInBlock + + " minItemsInBlock=" + + minItemsInBlock); + } + } + + @Override + public void write(Fields fields, NormsProducer norms) throws IOException { + // if (DEBUG) System.out.println("\nBTTW.write seg=" + segment); + + String lastField = null; + for (String field : fields) { + assert lastField == null || lastField.compareTo(field) < 0; + lastField = field; + + // if (DEBUG) System.out.println("\nBTTW.write seg=" + segment + " field=" + field); + Terms terms = fields.terms(field); + if (terms == null) { + continue; + } + + TermsEnum termsEnum = terms.iterator(); + TermsWriter termsWriter = new TermsWriter(fieldInfos.fieldInfo(field)); + while (true) { + BytesRef term = termsEnum.next(); + // if (DEBUG) System.out.println("BTTW: next term " + term); + + if (term == null) { + break; + } + + // if (DEBUG) System.out.println("write field=" + fieldInfo.name + " term=" + + // brToString(term)); + termsWriter.write(term, termsEnum, norms); + } + + termsWriter.finish(); + + // if (DEBUG) System.out.println("\nBTTW.write done seg=" + segment + " field=" + field); + } + } + + static long encodeOutput(long fp, boolean hasTerms, boolean isFloor) { + assert fp < (1L << 62); + return (fp << 2) + | (hasTerms ? Lucene40BlockTreeTermsReader.OUTPUT_FLAG_HAS_TERMS : 0) + | (isFloor ? Lucene40BlockTreeTermsReader.OUTPUT_FLAG_IS_FLOOR : 0); + } + + private static class PendingEntry { + public final boolean isTerm; + + protected PendingEntry(boolean isTerm) { + this.isTerm = isTerm; + } + } + + private static final class PendingTerm extends PendingEntry { + public final byte[] termBytes; + // stats + metadata + public final BlockTermState state; + + public PendingTerm(BytesRef term, BlockTermState state) { + super(true); + this.termBytes = new byte[term.length]; + System.arraycopy(term.bytes, term.offset, termBytes, 0, term.length); + this.state = state; + } + + @Override + public String toString() { + return "TERM: " + brToString(termBytes); + } + } + + // for debugging + @SuppressWarnings("unused") + static String brToString(BytesRef b) { + if (b == null) { + return "(null)"; + } else { + try { + return b.utf8ToString() + " " + b; + } catch (Throwable t) { + // If BytesRef isn't actually UTF8, or it's eg a + // prefix of UTF8 that ends mid-unicode-char, we + // fallback to hex: + return b.toString(); + } + } + } + + // for debugging + @SuppressWarnings("unused") + static String brToString(byte[] b) { + return brToString(new BytesRef(b)); + } + + private static final class PendingBlock extends PendingEntry { + public final BytesRef prefix; + public final long fp; + public FST index; + public List> subIndices; + public final boolean hasTerms; + public final boolean isFloor; + public final int floorLeadByte; + + public PendingBlock( + BytesRef prefix, + long fp, + boolean hasTerms, + boolean isFloor, + int floorLeadByte, + List> subIndices) { + super(false); + this.prefix = prefix; + this.fp = fp; + this.hasTerms = hasTerms; + this.isFloor = isFloor; + this.floorLeadByte = floorLeadByte; + this.subIndices = subIndices; + } + + @Override + public String toString() { + return "BLOCK: prefix=" + brToString(prefix); + } + + public void compileIndex( + List blocks, + ByteBuffersDataOutput scratchBytes, + IntsRefBuilder scratchIntsRef) + throws IOException { + + assert (isFloor && blocks.size() > 1) || (isFloor == false && blocks.size() == 1) + : "isFloor=" + isFloor + " blocks=" + blocks; + assert this == blocks.get(0); + + assert scratchBytes.size() == 0; + + // TODO: try writing the leading vLong in MSB order + // (opposite of what Lucene does today), for better + // outputs sharing in the FST + scratchBytes.writeVLong(encodeOutput(fp, hasTerms, isFloor)); + if (isFloor) { + scratchBytes.writeVInt(blocks.size() - 1); + for (int i = 1; i < blocks.size(); i++) { + PendingBlock sub = blocks.get(i); + assert sub.floorLeadByte != -1; + // if (DEBUG) { + // System.out.println(" write floorLeadByte=" + + // Integer.toHexString(sub.floorLeadByte&0xff)); + // } + scratchBytes.writeByte((byte) sub.floorLeadByte); + assert sub.fp > fp; + scratchBytes.writeVLong((sub.fp - fp) << 1 | (sub.hasTerms ? 1 : 0)); + } + } + + final ByteSequenceOutputs outputs = ByteSequenceOutputs.getSingleton(); + final FSTCompiler fstCompiler = + new FSTCompiler.Builder<>(FST.INPUT_TYPE.BYTE1, outputs).build(); + // if (DEBUG) { + // System.out.println(" compile index for prefix=" + prefix); + // } + // indexBuilder.DEBUG = false; + final byte[] bytes = scratchBytes.toArrayCopy(); + assert bytes.length > 0; + fstCompiler.add(Util.toIntsRef(prefix, scratchIntsRef), new BytesRef(bytes, 0, bytes.length)); + scratchBytes.reset(); + + // Copy over index for all sub-blocks + for (PendingBlock block : blocks) { + if (block.subIndices != null) { + for (FST subIndex : block.subIndices) { + append(fstCompiler, subIndex, scratchIntsRef); + } + block.subIndices = null; + } + } + + index = FST.fromFSTReader(fstCompiler.compile(), fstCompiler.getFSTReader()); + + assert subIndices == null; + + /* + Writer w = new OutputStreamWriter(new FileOutputStream("out.dot")); + Util.toDot(index, w, false, false); + System.out.println("SAVED to out.dot"); + w.close(); + */ + } + + // TODO: maybe we could add bulk-add method to + // Builder? Takes FST and unions it w/ current + // FST. + private void append( + FSTCompiler fstCompiler, FST subIndex, IntsRefBuilder scratchIntsRef) + throws IOException { + final BytesRefFSTEnum subIndexEnum = new BytesRefFSTEnum<>(subIndex); + BytesRefFSTEnum.InputOutput indexEnt; + while ((indexEnt = subIndexEnum.next()) != null) { + // if (DEBUG) { + // System.out.println(" add sub=" + indexEnt.input + " " + indexEnt.input + " output=" + // + indexEnt.output); + // } + fstCompiler.add(Util.toIntsRef(indexEnt.input, scratchIntsRef), indexEnt.output); + } + } + } + + private final ByteBuffersDataOutput scratchBytes = ByteBuffersDataOutput.newResettableInstance(); + private final IntsRefBuilder scratchIntsRef = new IntsRefBuilder(); + + static final BytesRef EMPTY_BYTES_REF = new BytesRef(); + + private static class StatsWriter { + + private final DataOutput out; + private final boolean hasFreqs; + private int singletonCount; + + StatsWriter(DataOutput out, boolean hasFreqs) { + this.out = out; + this.hasFreqs = hasFreqs; + } + + void add(int df, long ttf) throws IOException { + // Singletons (DF==1, TTF==1) are run-length encoded + if (df == 1 && (hasFreqs == false || ttf == 1)) { + singletonCount++; + } else { + finish(); + out.writeVInt(df << 1); + if (hasFreqs) { + out.writeVLong(ttf - df); + } + } + } + + void finish() throws IOException { + if (singletonCount > 0) { + out.writeVInt(((singletonCount - 1) << 1) | 1); + singletonCount = 0; + } + } + } + + class TermsWriter { + private final FieldInfo fieldInfo; + private long numTerms; + final FixedBitSet docsSeen; + long sumTotalTermFreq; + long sumDocFreq; + long indexStartFP; + + // Records index into pending where the current prefix at that + // length "started"; for example, if current term starts with 't', + // startsByPrefix[0] is the index into pending for the first + // term/sub-block starting with 't'. We use this to figure out when + // to write a new block: + private final BytesRefBuilder lastTerm = new BytesRefBuilder(); + private int[] prefixStarts = new int[8]; + + // Pending stack of terms and blocks. As terms arrive (in sorted order) + // we append to this stack, and once the top of the stack has enough + // terms starting with a common prefix, we write a new block with + // those terms and replace those terms in the stack with a new block: + private final List pending = new ArrayList<>(); + + // Reused in writeBlocks: + private final List newBlocks = new ArrayList<>(); + + private PendingTerm firstPendingTerm; + private PendingTerm lastPendingTerm; + + /** Writes the top count entries in pending, using prevTerm to compute the prefix. */ + void writeBlocks(int prefixLength, int count) throws IOException { + + assert count > 0; + + // if (DEBUG2) { + // BytesRef br = new BytesRef(lastTerm.bytes()); + // br.length = prefixLength; + // System.out.println("writeBlocks: seg=" + segment + " prefix=" + brToString(br) + " count=" + // + count); + // } + + // Root block better write all remaining pending entries: + assert prefixLength > 0 || count == pending.size(); + + int lastSuffixLeadLabel = -1; + + // True if we saw at least one term in this block (we record if a block + // only points to sub-blocks in the terms index so we can avoid seeking + // to it when we are looking for a term): + boolean hasTerms = false; + boolean hasSubBlocks = false; + + int start = pending.size() - count; + int end = pending.size(); + int nextBlockStart = start; + int nextFloorLeadLabel = -1; + + for (int i = start; i < end; i++) { + + PendingEntry ent = pending.get(i); + + int suffixLeadLabel; + + if (ent.isTerm) { + PendingTerm term = (PendingTerm) ent; + if (term.termBytes.length == prefixLength) { + // Suffix is 0, i.e. prefix 'foo' and term is + // 'foo' so the term has empty string suffix + // in this block + assert lastSuffixLeadLabel == -1 + : "i=" + i + " lastSuffixLeadLabel=" + lastSuffixLeadLabel; + suffixLeadLabel = -1; + } else { + suffixLeadLabel = term.termBytes[prefixLength] & 0xff; + } + } else { + PendingBlock block = (PendingBlock) ent; + assert block.prefix.length > prefixLength; + suffixLeadLabel = block.prefix.bytes[block.prefix.offset + prefixLength] & 0xff; + } + // if (DEBUG) System.out.println(" i=" + i + " ent=" + ent + " suffixLeadLabel=" + + // suffixLeadLabel); + + if (suffixLeadLabel != lastSuffixLeadLabel) { + int itemsInBlock = i - nextBlockStart; + if (itemsInBlock >= minItemsInBlock && end - nextBlockStart > maxItemsInBlock) { + // The count is too large for one block, so we must break it into "floor" blocks, where + // we record + // the leading label of the suffix of the first term in each floor block, so at search + // time we can + // jump to the right floor block. We just use a naive greedy segmenter here: make a new + // floor + // block as soon as we have at least minItemsInBlock. This is not always best: it often + // produces + // a too-small block as the final block: + boolean isFloor = itemsInBlock < count; + newBlocks.add( + writeBlock( + prefixLength, + isFloor, + nextFloorLeadLabel, + nextBlockStart, + i, + hasTerms, + hasSubBlocks)); + + hasTerms = false; + hasSubBlocks = false; + nextFloorLeadLabel = suffixLeadLabel; + nextBlockStart = i; + } + + lastSuffixLeadLabel = suffixLeadLabel; + } + + if (ent.isTerm) { + hasTerms = true; + } else { + hasSubBlocks = true; + } + } + + // Write last block, if any: + if (nextBlockStart < end) { + int itemsInBlock = end - nextBlockStart; + boolean isFloor = itemsInBlock < count; + newBlocks.add( + writeBlock( + prefixLength, + isFloor, + nextFloorLeadLabel, + nextBlockStart, + end, + hasTerms, + hasSubBlocks)); + } + + assert newBlocks.isEmpty() == false; + + PendingBlock firstBlock = newBlocks.get(0); + + assert firstBlock.isFloor || newBlocks.size() == 1; + + firstBlock.compileIndex(newBlocks, scratchBytes, scratchIntsRef); + + // Remove slice from the top of the pending stack, that we just wrote: + pending.subList(pending.size() - count, pending.size()).clear(); + + // Append new block + pending.add(firstBlock); + + newBlocks.clear(); + } + + private boolean allEqual(byte[] b, int startOffset, int endOffset, byte value) { + Objects.checkFromToIndex(startOffset, endOffset, b.length); + for (int i = startOffset; i < endOffset; ++i) { + if (b[i] != value) { + return false; + } + } + return true; + } + + /** + * Writes the specified slice (start is inclusive, end is exclusive) from pending stack as a new + * block. If isFloor is true, there were too many (more than maxItemsInBlock) entries sharing + * the same prefix, and so we broke it into multiple floor blocks where we record the starting + * label of the suffix of each floor block. + */ + private PendingBlock writeBlock( + int prefixLength, + boolean isFloor, + int floorLeadLabel, + int start, + int end, + boolean hasTerms, + boolean hasSubBlocks) + throws IOException { + + assert end > start; + + long startFP = termsOut.getFilePointer(); + + boolean hasFloorLeadLabel = isFloor && floorLeadLabel != -1; + + final BytesRef prefix = new BytesRef(prefixLength + (hasFloorLeadLabel ? 1 : 0)); + System.arraycopy(lastTerm.get().bytes, 0, prefix.bytes, 0, prefixLength); + prefix.length = prefixLength; + + // if (DEBUG2) System.out.println(" writeBlock field=" + fieldInfo.name + " prefix=" + + // brToString(prefix) + " fp=" + startFP + " isFloor=" + isFloor + " isLastInFloor=" + (end == + // pending.size()) + " floorLeadLabel=" + floorLeadLabel + " start=" + start + " end=" + end + + // " hasTerms=" + hasTerms + " hasSubBlocks=" + hasSubBlocks); + + // Write block header: + int numEntries = end - start; + int code = numEntries << 1; + if (end == pending.size()) { + // Last block: + code |= 1; + } + termsOut.writeVInt(code); + + /* + if (DEBUG) { + System.out.println(" writeBlock " + (isFloor ? "(floor) " : "") + "seg=" + segment + " pending.size()=" + pending.size() + " prefixLength=" + prefixLength + " indexPrefix=" + brToString(prefix) + " entCount=" + (end-start+1) + " startFP=" + startFP + (isFloor ? (" floorLeadLabel=" + Integer.toHexString(floorLeadLabel)) : "")); + } + */ + + // 1st pass: pack term suffix bytes into byte[] blob + // TODO: cutover to bulk int codec... simple64? + + // We optimize the leaf block case (block has only terms), writing a more + // compact format in this case: + boolean isLeafBlock = hasSubBlocks == false; + + // System.out.println(" isLeaf=" + isLeafBlock); + + final List> subIndices; + + boolean absolute = true; + + if (isLeafBlock) { + // Block contains only ordinary terms: + subIndices = null; + StatsWriter statsWriter = + new StatsWriter(this.statsWriter, fieldInfo.getIndexOptions() != IndexOptions.DOCS); + for (int i = start; i < end; i++) { + PendingEntry ent = pending.get(i); + assert ent.isTerm : "i=" + i; + + PendingTerm term = (PendingTerm) ent; + + assert StringHelper.startsWith(term.termBytes, prefix) + : "term.term=" + new BytesRef(term.termBytes) + " prefix=" + prefix; + BlockTermState state = term.state; + final int suffix = term.termBytes.length - prefixLength; + // if (DEBUG2) { + // BytesRef suffixBytes = new BytesRef(suffix); + // System.arraycopy(term.termBytes, prefixLength, suffixBytes.bytes, 0, suffix); + // suffixBytes.length = suffix; + // System.out.println(" write term suffix=" + brToString(suffixBytes)); + // } + + // For leaf block we write suffix straight + suffixLengthsWriter.writeVInt(suffix); + suffixWriter.append(term.termBytes, prefixLength, suffix); + assert floorLeadLabel == -1 || (term.termBytes[prefixLength] & 0xff) >= floorLeadLabel; + + // Write term stats, to separate byte[] blob: + statsWriter.add(state.docFreq, state.totalTermFreq); + + // Write term meta data + postingsWriter.encodeTerm(metaWriter, fieldInfo, state, absolute); + absolute = false; + } + statsWriter.finish(); + } else { + // Block has at least one prefix term or a sub block: + subIndices = new ArrayList<>(); + StatsWriter statsWriter = + new StatsWriter(this.statsWriter, fieldInfo.getIndexOptions() != IndexOptions.DOCS); + for (int i = start; i < end; i++) { + PendingEntry ent = pending.get(i); + if (ent.isTerm) { + PendingTerm term = (PendingTerm) ent; + + assert StringHelper.startsWith(term.termBytes, prefix) + : "term.term=" + new BytesRef(term.termBytes) + " prefix=" + prefix; + BlockTermState state = term.state; + final int suffix = term.termBytes.length - prefixLength; + // if (DEBUG2) { + // BytesRef suffixBytes = new BytesRef(suffix); + // System.arraycopy(term.termBytes, prefixLength, suffixBytes.bytes, 0, suffix); + // suffixBytes.length = suffix; + // System.out.println(" write term suffix=" + brToString(suffixBytes)); + // } + + // For non-leaf block we borrow 1 bit to record + // if entry is term or sub-block, and 1 bit to record if + // it's a prefix term. Terms cannot be larger than ~32 KB + // so we won't run out of bits: + + suffixLengthsWriter.writeVInt(suffix << 1); + suffixWriter.append(term.termBytes, prefixLength, suffix); + + // Write term stats, to separate byte[] blob: + statsWriter.add(state.docFreq, state.totalTermFreq); + + // TODO: now that terms dict "sees" these longs, + // we can explore better column-stride encodings + // to encode all long[0]s for this block at + // once, all long[1]s, etc., e.g. using + // Simple64. Alternatively, we could interleave + // stats + meta ... no reason to have them + // separate anymore: + + // Write term meta data + postingsWriter.encodeTerm(metaWriter, fieldInfo, state, absolute); + absolute = false; + } else { + PendingBlock block = (PendingBlock) ent; + assert StringHelper.startsWith(block.prefix, prefix); + final int suffix = block.prefix.length - prefixLength; + assert StringHelper.startsWith(block.prefix, prefix); + + assert suffix > 0; + + // For non-leaf block we borrow 1 bit to record + // if entry is term or sub-block:f + suffixLengthsWriter.writeVInt((suffix << 1) | 1); + suffixWriter.append(block.prefix.bytes, prefixLength, suffix); + + // if (DEBUG2) { + // BytesRef suffixBytes = new BytesRef(suffix); + // System.arraycopy(block.prefix.bytes, prefixLength, suffixBytes.bytes, 0, suffix); + // suffixBytes.length = suffix; + // System.out.println(" write sub-block suffix=" + brToString(suffixBytes) + " + // subFP=" + block.fp + " subCode=" + (startFP-block.fp) + " floor=" + block.isFloor); + // } + + assert floorLeadLabel == -1 + || (block.prefix.bytes[prefixLength] & 0xff) >= floorLeadLabel + : "floorLeadLabel=" + + floorLeadLabel + + " suffixLead=" + + (block.prefix.bytes[prefixLength] & 0xff); + assert block.fp < startFP; + + suffixLengthsWriter.writeVLong(startFP - block.fp); + subIndices.add(block.index); + } + } + statsWriter.finish(); + + assert subIndices.size() != 0; + } + + // Write suffixes byte[] blob to terms dict output, either uncompressed, compressed with LZ4 + // or with LowercaseAsciiCompression. + CompressionAlgorithm compressionAlg = CompressionAlgorithm.NO_COMPRESSION; + // If there are 2 suffix bytes or less per term, then we don't bother compressing as suffix + // are unlikely what + // makes the terms dictionary large, and it also tends to be frequently the case for dense IDs + // like + // auto-increment IDs, so not compressing in that case helps not hurt ID lookups by too much. + // We also only start compressing when the prefix length is greater than 2 since blocks whose + // prefix length is + // 1 or 2 always all get visited when running a fuzzy query whose max number of edits is 2. + if (suffixWriter.length() > 2L * numEntries && prefixLength > 2) { + // LZ4 inserts references whenever it sees duplicate strings of 4 chars or more, so only try + // it out if the + // average suffix length is greater than 6. + if (suffixWriter.length() > 6L * numEntries) { + LZ4.compress( + suffixWriter.bytes(), 0, suffixWriter.length(), spareWriter, compressionHashTable); + if (spareWriter.size() < suffixWriter.length() - (suffixWriter.length() >>> 2)) { + // LZ4 saved more than 25%, go for it + compressionAlg = CompressionAlgorithm.LZ4; + } + } + if (compressionAlg == CompressionAlgorithm.NO_COMPRESSION) { + spareWriter.reset(); + if (spareBytes.length < suffixWriter.length()) { + spareBytes = new byte[ArrayUtil.oversize(suffixWriter.length(), 1)]; + } + if (LowercaseAsciiCompression.compress( + suffixWriter.bytes(), suffixWriter.length(), spareBytes, spareWriter)) { + compressionAlg = CompressionAlgorithm.LOWERCASE_ASCII; + } + } + } + long token = ((long) suffixWriter.length()) << 3; + if (isLeafBlock) { + token |= 0x04; + } + token |= compressionAlg.code; + termsOut.writeVLong(token); + if (compressionAlg == CompressionAlgorithm.NO_COMPRESSION) { + termsOut.writeBytes(suffixWriter.bytes(), suffixWriter.length()); + } else { + spareWriter.copyTo(termsOut); + } + suffixWriter.setLength(0); + spareWriter.reset(); + + // Write suffix lengths + final int numSuffixBytes = Math.toIntExact(suffixLengthsWriter.size()); + spareBytes = ArrayUtil.grow(spareBytes, numSuffixBytes); + suffixLengthsWriter.copyTo(new ByteArrayDataOutput(spareBytes)); + suffixLengthsWriter.reset(); + if (allEqual(spareBytes, 1, numSuffixBytes, spareBytes[0])) { + // Structured fields like IDs often have most values of the same length + termsOut.writeVInt((numSuffixBytes << 1) | 1); + termsOut.writeByte(spareBytes[0]); + } else { + termsOut.writeVInt(numSuffixBytes << 1); + termsOut.writeBytes(spareBytes, numSuffixBytes); + } + + // Stats + final int numStatsBytes = Math.toIntExact(statsWriter.size()); + termsOut.writeVInt(numStatsBytes); + statsWriter.copyTo(termsOut); + statsWriter.reset(); + + // Write term meta data byte[] blob + termsOut.writeVInt((int) metaWriter.size()); + metaWriter.copyTo(termsOut); + metaWriter.reset(); + + // if (DEBUG) { + // System.out.println(" fpEnd=" + out.getFilePointer()); + // } + + if (hasFloorLeadLabel) { + // We already allocated to length+1 above: + prefix.bytes[prefix.length++] = (byte) floorLeadLabel; + } + + return new PendingBlock(prefix, startFP, hasTerms, isFloor, floorLeadLabel, subIndices); + } + + TermsWriter(FieldInfo fieldInfo) { + this.fieldInfo = fieldInfo; + assert fieldInfo.getIndexOptions() != IndexOptions.NONE; + docsSeen = new FixedBitSet(maxDoc); + postingsWriter.setField(fieldInfo); + } + + /** Writes one term's worth of postings. */ + public void write(BytesRef text, TermsEnum termsEnum, NormsProducer norms) throws IOException { + /* + if (DEBUG) { + int[] tmp = new int[lastTerm.length]; + System.arraycopy(prefixStarts, 0, tmp, 0, tmp.length); + System.out.println("BTTW: write term=" + brToString(text) + " prefixStarts=" + Arrays.toString(tmp) + " pending.size()=" + pending.size()); + } + */ + + BlockTermState state = postingsWriter.writeTerm(text, termsEnum, docsSeen, norms); + if (state != null) { + + assert state.docFreq != 0; + assert fieldInfo.getIndexOptions() == IndexOptions.DOCS + || state.totalTermFreq >= state.docFreq + : "postingsWriter=" + postingsWriter; + pushTerm(text); + + PendingTerm term = new PendingTerm(text, state); + pending.add(term); + // if (DEBUG) System.out.println(" add pending term = " + text + " pending.size()=" + + // pending.size()); + + sumDocFreq += state.docFreq; + sumTotalTermFreq += state.totalTermFreq; + numTerms++; + if (firstPendingTerm == null) { + firstPendingTerm = term; + } + lastPendingTerm = term; + } + } + + /** Pushes the new term to the top of the stack, and writes new blocks. */ + private void pushTerm(BytesRef text) throws IOException { + // Find common prefix between last term and current term: + int prefixLength = + Arrays.mismatch( + lastTerm.bytes(), + 0, + lastTerm.length(), + text.bytes, + text.offset, + text.offset + text.length); + if (prefixLength == -1) { // Only happens for the first term, if it is empty + assert lastTerm.length() == 0; + prefixLength = 0; + } + + // if (DEBUG) System.out.println(" shared=" + pos + " lastTerm.length=" + lastTerm.length); + + // Close the "abandoned" suffix now: + for (int i = lastTerm.length() - 1; i >= prefixLength; i--) { + + // How many items on top of the stack share the current suffix + // we are closing: + int prefixTopSize = pending.size() - prefixStarts[i]; + if (prefixTopSize >= minItemsInBlock) { + // if (DEBUG) System.out.println("pushTerm i=" + i + " prefixTopSize=" + prefixTopSize + " + // minItemsInBlock=" + minItemsInBlock); + writeBlocks(i + 1, prefixTopSize); + prefixStarts[i] -= prefixTopSize - 1; + } + } + + if (prefixStarts.length < text.length) { + prefixStarts = ArrayUtil.grow(prefixStarts, text.length); + } + + // Init new tail: + for (int i = prefixLength; i < text.length; i++) { + prefixStarts[i] = pending.size(); + } + + lastTerm.copyBytes(text); + } + + // Finishes all terms in this field + public void finish() throws IOException { + if (numTerms > 0) { + // if (DEBUG) System.out.println("BTTW: finish prefixStarts=" + + // Arrays.toString(prefixStarts)); + + // Add empty term to force closing of all final blocks: + pushTerm(new BytesRef()); + + // TODO: if pending.size() is already 1 with a non-zero prefix length + // we can save writing a "degenerate" root block, but we have to + // fix all the places that assume the root block's prefix is the empty string: + pushTerm(new BytesRef()); + writeBlocks(0, pending.size()); + + // We better have one final "root" block: + assert pending.size() == 1 && !pending.get(0).isTerm + : "pending.size()=" + pending.size() + " pending=" + pending; + final PendingBlock root = (PendingBlock) pending.get(0); + assert root.prefix.length == 0; + assert root.index.getEmptyOutput() != null; + + // Write FST to index + indexStartFP = indexOut.getFilePointer(); + root.index.save(indexOut, indexOut); + // System.out.println(" write FST " + indexStartFP + " field=" + fieldInfo.name); + + /* + if (DEBUG) { + final String dotFileName = segment + "_" + fieldInfo.name + ".dot"; + Writer w = new OutputStreamWriter(new FileOutputStream(dotFileName)); + Util.toDot(root.index, w, false, false); + System.out.println("SAVED to " + dotFileName); + w.close(); + } + */ + assert firstPendingTerm != null; + BytesRef minTerm = new BytesRef(firstPendingTerm.termBytes); + + assert lastPendingTerm != null; + BytesRef maxTerm = new BytesRef(lastPendingTerm.termBytes); + + fields.add( + new FieldMetaData( + fieldInfo, + ((PendingBlock) pending.get(0)).index.getEmptyOutput(), + numTerms, + indexStartFP, + sumTotalTermFreq, + sumDocFreq, + docsSeen.cardinality(), + minTerm, + maxTerm)); + } else { + assert sumTotalTermFreq == 0 + || fieldInfo.getIndexOptions() == IndexOptions.DOCS && sumTotalTermFreq == -1; + assert sumDocFreq == 0; + assert docsSeen.cardinality() == 0; + } + } + + private final ByteBuffersDataOutput suffixLengthsWriter = + ByteBuffersDataOutput.newResettableInstance(); + private final BytesRefBuilder suffixWriter = new BytesRefBuilder(); + private final ByteBuffersDataOutput statsWriter = ByteBuffersDataOutput.newResettableInstance(); + private final ByteBuffersDataOutput metaWriter = ByteBuffersDataOutput.newResettableInstance(); + private final ByteBuffersDataOutput spareWriter = ByteBuffersDataOutput.newResettableInstance(); + private byte[] spareBytes = BytesRef.EMPTY_BYTES; + private final LZ4.HighCompressionHashTable compressionHashTable = + new LZ4.HighCompressionHashTable(); + } + + private boolean closed; + + @Override + public void close() throws IOException { + if (closed) { + return; + } + closed = true; + + boolean success = false; + try { + + final long dirStart = termsOut.getFilePointer(); + final long indexDirStart = indexOut.getFilePointer(); + + termsOut.writeVInt(fields.size()); + + for (FieldMetaData field : fields) { + // System.out.println(" field " + field.fieldInfo.name + " " + field.numTerms + " terms"); + termsOut.writeVInt(field.fieldInfo.number); + assert field.numTerms > 0; + termsOut.writeVLong(field.numTerms); + termsOut.writeVInt(field.rootCode.length); + termsOut.writeBytes(field.rootCode.bytes, field.rootCode.offset, field.rootCode.length); + assert field.fieldInfo.getIndexOptions() != IndexOptions.NONE; + if (field.fieldInfo.getIndexOptions() != IndexOptions.DOCS) { + termsOut.writeVLong(field.sumTotalTermFreq); + } + termsOut.writeVLong(field.sumDocFreq); + termsOut.writeVInt(field.docCount); + indexOut.writeVLong(field.indexStartFP); + writeBytesRef(termsOut, field.minTerm); + writeBytesRef(termsOut, field.maxTerm); + } + writeTrailer(termsOut, dirStart); + CodecUtil.writeFooter(termsOut); + writeIndexTrailer(indexOut, indexDirStart); + CodecUtil.writeFooter(indexOut); + success = true; + } finally { + if (success) { + IOUtils.close(termsOut, indexOut, postingsWriter); + } else { + IOUtils.closeWhileHandlingException(termsOut, indexOut, postingsWriter); + } + } + } + + private static void writeBytesRef(IndexOutput out, BytesRef bytes) throws IOException { + out.writeVInt(bytes.length); + out.writeBytes(bytes.bytes, bytes.offset, bytes.length); + } +} diff --git a/lucene/backward-codecs/src/test/org/apache/lucene/backward_codecs/lucene50/Lucene50RWPostingsFormat.java b/lucene/backward-codecs/src/test/org/apache/lucene/backward_codecs/lucene50/Lucene50RWPostingsFormat.java index 7b5f0b3ebc2..fb042a1a9ad 100644 --- a/lucene/backward-codecs/src/test/org/apache/lucene/backward_codecs/lucene50/Lucene50RWPostingsFormat.java +++ b/lucene/backward-codecs/src/test/org/apache/lucene/backward_codecs/lucene50/Lucene50RWPostingsFormat.java @@ -17,7 +17,7 @@ package org.apache.lucene.backward_codecs.lucene50; import java.io.IOException; -import org.apache.lucene.backward_codecs.lucene40.blocktree.Lucene40BlockTreeTermsWriter; +import org.apache.lucene.backward_codecs.lucene40.blocktree.Lucene40BlockTreeTermsWriterV5; import org.apache.lucene.codecs.FieldsConsumer; import org.apache.lucene.codecs.PostingsWriterBase; import org.apache.lucene.index.SegmentWriteState; @@ -31,11 +31,11 @@ public class Lucene50RWPostingsFormat extends Lucene50PostingsFormat { boolean success = false; try { FieldsConsumer ret = - new Lucene40BlockTreeTermsWriter( + new Lucene40BlockTreeTermsWriterV5( state, postingsWriter, - Lucene40BlockTreeTermsWriter.DEFAULT_MIN_BLOCK_SIZE, - Lucene40BlockTreeTermsWriter.DEFAULT_MAX_BLOCK_SIZE); + Lucene40BlockTreeTermsWriterV5.DEFAULT_MIN_BLOCK_SIZE, + Lucene40BlockTreeTermsWriterV5.DEFAULT_MAX_BLOCK_SIZE); success = true; return ret; } finally { diff --git a/lucene/backward-codecs/src/test/org/apache/lucene/backward_index/TestAncientIndicesCompatibility.java b/lucene/backward-codecs/src/test/org/apache/lucene/backward_index/TestAncientIndicesCompatibility.java index 938d0de6b90..88adfadf1c8 100644 --- a/lucene/backward-codecs/src/test/org/apache/lucene/backward_index/TestAncientIndicesCompatibility.java +++ b/lucene/backward-codecs/src/test/org/apache/lucene/backward_index/TestAncientIndicesCompatibility.java @@ -196,6 +196,7 @@ public class TestAncientIndicesCompatibility extends LuceneTestCase { ByteArrayOutputStream bos = new ByteArrayOutputStream(1024); CheckIndex checker = new CheckIndex(dir); checker.setInfoStream(new PrintStream(bos, false, UTF_8)); + checker.setLevel(CheckIndex.Level.MIN_LEVEL_FOR_INTEGRITY_CHECKS); CheckIndex.Status indexStatus = checker.checkIndex(); if (version.startsWith("8.")) { assertTrue(indexStatus.clean); From 19fe1a56f7add4b1601037d11a097c0f1c10036b Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Sat, 29 Jun 2024 13:54:32 -0400 Subject: [PATCH 23/42] Fix more vector similarity query tests (#13530) --- .../apache/lucene/search/BaseVectorSimilarityQueryTestCase.java | 1 + 1 file changed, 1 insertion(+) diff --git a/lucene/core/src/test/org/apache/lucene/search/BaseVectorSimilarityQueryTestCase.java b/lucene/core/src/test/org/apache/lucene/search/BaseVectorSimilarityQueryTestCase.java index f0bce64acd0..3347b9478dd 100644 --- a/lucene/core/src/test/org/apache/lucene/search/BaseVectorSimilarityQueryTestCase.java +++ b/lucene/core/src/test/org/apache/lucene/search/BaseVectorSimilarityQueryTestCase.java @@ -131,6 +131,7 @@ abstract class BaseVectorSimilarityQueryTestCase< try (Directory indexStore = getIndexStore(getRandomVectors(numDocs, dim)); IndexReader reader = DirectoryReader.open(indexStore)) { IndexSearcher searcher = newSearcher(reader); + assumeTrue("graph is disconnected", HnswTestUtil.graphIsConnected(reader, vectorField)); // All vectors are above -Infinity Query query1 = From 5f91d609ea5ec734f08a8218c69cb5db4fe7db7b Mon Sep 17 00:00:00 2001 From: Stefan Vodita <41467371+stefanvodita@users.noreply.github.com> Date: Mon, 1 Jul 2024 09:09:51 +0100 Subject: [PATCH 24/42] Make Gradle dashboard easy to find by adding a badge (#13476) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index fe523af81b2..c613a16986e 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Apache Lucene is a high-performance, full-featured text search engine library written in Java. [![Build Status](https://ci-builds.apache.org/job/Lucene/job/Lucene-Artifacts-main/badge/icon?subject=Lucene)](https://ci-builds.apache.org/job/Lucene/job/Lucene-Artifacts-main/) +[![Revved up by Develocity](https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.apache.org/scans?search.buildToolType=gradle&search.rootProjectNames=lucene-root) ## Online Documentation From 3cd406e783739a6bdcfc13d14b12640ba63fcdfa Mon Sep 17 00:00:00 2001 From: zhouhui Date: Tue, 2 Jul 2024 00:29:10 +0800 Subject: [PATCH 25/42] Remove unused segNo calculation in IndexWriter.doFlush (#13491) --- .../src/java/org/apache/lucene/index/IndexWriter.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lucene/core/src/java/org/apache/lucene/index/IndexWriter.java b/lucene/core/src/java/org/apache/lucene/index/IndexWriter.java index 44d8bee8460..83c2cbdaf1f 100644 --- a/lucene/core/src/java/org/apache/lucene/index/IndexWriter.java +++ b/lucene/core/src/java/org/apache/lucene/index/IndexWriter.java @@ -4270,13 +4270,7 @@ public class IndexWriter synchronized (fullFlushLock) { boolean flushSuccess = false; try { - long seqNo = docWriter.flushAllThreads(); - if (seqNo < 0) { - seqNo = -seqNo; - anyChanges = true; - } else { - anyChanges = false; - } + anyChanges = (docWriter.flushAllThreads() < 0); if (!anyChanges) { // flushCount is incremented in flushAllThreads flushCount.incrementAndGet(); From 0ad270d8b0d4ca1e8ef0ddf2b32947192b51b224 Mon Sep 17 00:00:00 2001 From: Christine Poerschke Date: Mon, 1 Jul 2024 17:31:02 +0100 Subject: [PATCH 26/42] [Abstract]Knn[Byte|Float]VectorQuery tweaks: reduce duplicate method calls (#13528) * reduce LeafReaderContext.reader()[.maxDoc()] calls in AbstractKnnVectorQuery.getLeafResults * reduce IndexReader.leaves() calls in AbstractKnnVectorQuery.findSegmentStarts * reduce LeafReaderContext.reader() calls in Knn(Byte|Float)VectorQuery.approximateSearch --- .../lucene/search/AbstractKnnVectorQuery.java | 15 ++++++++------- .../apache/lucene/search/KnnByteVectorQuery.java | 13 ++++++++----- .../apache/lucene/search/KnnFloatVectorQuery.java | 13 ++++++++----- .../lucene/search/TestKnnFloatVectorQuery.java | 2 +- 4 files changed, 25 insertions(+), 18 deletions(-) diff --git a/lucene/core/src/java/org/apache/lucene/search/AbstractKnnVectorQuery.java b/lucene/core/src/java/org/apache/lucene/search/AbstractKnnVectorQuery.java index 8fceb0cfec5..c0ce4eea3c6 100644 --- a/lucene/core/src/java/org/apache/lucene/search/AbstractKnnVectorQuery.java +++ b/lucene/core/src/java/org/apache/lucene/search/AbstractKnnVectorQuery.java @@ -28,6 +28,7 @@ import java.util.concurrent.Callable; import org.apache.lucene.codecs.KnnVectorsReader; import org.apache.lucene.index.FieldInfo; import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.LeafReader; import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.index.QueryTimeout; import org.apache.lucene.search.knn.KnnCollectorManager; @@ -120,8 +121,8 @@ abstract class AbstractKnnVectorQuery extends Query { Weight filterWeight, TimeLimitingKnnCollectorManager timeLimitingKnnCollectorManager) throws IOException { - Bits liveDocs = ctx.reader().getLiveDocs(); - int maxDoc = ctx.reader().maxDoc(); + final LeafReader reader = ctx.reader(); + final Bits liveDocs = reader.getLiveDocs(); if (filterWeight == null) { return approximateSearch(ctx, liveDocs, Integer.MAX_VALUE, timeLimitingKnnCollectorManager); @@ -132,7 +133,7 @@ abstract class AbstractKnnVectorQuery extends Query { return NO_RESULTS; } - BitSet acceptDocs = createBitSet(scorer.iterator(), liveDocs, maxDoc); + BitSet acceptDocs = createBitSet(scorer.iterator(), liveDocs, reader.maxDoc()); final int cost = acceptDocs.cardinality(); QueryTimeout queryTimeout = timeLimitingKnnCollectorManager.getQueryTimeout(); @@ -267,19 +268,19 @@ abstract class AbstractKnnVectorQuery extends Query { docs[i] = topK.scoreDocs[i].doc; scores[i] = topK.scoreDocs[i].score; } - int[] segmentStarts = findSegmentStarts(reader, docs); + int[] segmentStarts = findSegmentStarts(reader.leaves(), docs); return new DocAndScoreQuery(docs, scores, maxScore, segmentStarts, reader.getContext().id()); } - static int[] findSegmentStarts(IndexReader reader, int[] docs) { - int[] starts = new int[reader.leaves().size() + 1]; + static int[] findSegmentStarts(List leaves, int[] docs) { + int[] starts = new int[leaves.size() + 1]; starts[starts.length - 1] = docs.length; if (starts.length == 2) { return starts; } int resultIndex = 0; for (int i = 1; i < starts.length - 1; i++) { - int upper = reader.leaves().get(i).docBase; + int upper = leaves.get(i).docBase; resultIndex = Arrays.binarySearch(docs, resultIndex, docs.length, upper); if (resultIndex < 0) { resultIndex = -1 - resultIndex; diff --git a/lucene/core/src/java/org/apache/lucene/search/KnnByteVectorQuery.java b/lucene/core/src/java/org/apache/lucene/search/KnnByteVectorQuery.java index 5b60e680e10..db5ae4a0d9d 100644 --- a/lucene/core/src/java/org/apache/lucene/search/KnnByteVectorQuery.java +++ b/lucene/core/src/java/org/apache/lucene/search/KnnByteVectorQuery.java @@ -23,6 +23,7 @@ import org.apache.lucene.codecs.KnnVectorsReader; import org.apache.lucene.document.KnnFloatVectorField; import org.apache.lucene.index.ByteVectorValues; import org.apache.lucene.index.FieldInfo; +import org.apache.lucene.index.LeafReader; import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.search.knn.KnnCollectorManager; import org.apache.lucene.util.ArrayUtil; @@ -83,24 +84,26 @@ public class KnnByteVectorQuery extends AbstractKnnVectorQuery { KnnCollectorManager knnCollectorManager) throws IOException { KnnCollector knnCollector = knnCollectorManager.newCollector(visitedLimit, context); - ByteVectorValues byteVectorValues = context.reader().getByteVectorValues(field); + LeafReader reader = context.reader(); + ByteVectorValues byteVectorValues = reader.getByteVectorValues(field); if (byteVectorValues == null) { - ByteVectorValues.checkField(context.reader(), field); + ByteVectorValues.checkField(reader, field); return NO_RESULTS; } if (Math.min(knnCollector.k(), byteVectorValues.size()) == 0) { return NO_RESULTS; } - context.reader().searchNearestVectors(field, target, knnCollector, acceptDocs); + reader.searchNearestVectors(field, target, knnCollector, acceptDocs); TopDocs results = knnCollector.topDocs(); return results != null ? results : NO_RESULTS; } @Override VectorScorer createVectorScorer(LeafReaderContext context, FieldInfo fi) throws IOException { - ByteVectorValues vectorValues = context.reader().getByteVectorValues(field); + LeafReader reader = context.reader(); + ByteVectorValues vectorValues = reader.getByteVectorValues(field); if (vectorValues == null) { - ByteVectorValues.checkField(context.reader(), field); + ByteVectorValues.checkField(reader, field); return null; } return vectorValues.scorer(target); diff --git a/lucene/core/src/java/org/apache/lucene/search/KnnFloatVectorQuery.java b/lucene/core/src/java/org/apache/lucene/search/KnnFloatVectorQuery.java index b06e81eb5d8..585893fa3c2 100644 --- a/lucene/core/src/java/org/apache/lucene/search/KnnFloatVectorQuery.java +++ b/lucene/core/src/java/org/apache/lucene/search/KnnFloatVectorQuery.java @@ -23,6 +23,7 @@ import org.apache.lucene.codecs.KnnVectorsReader; import org.apache.lucene.document.KnnFloatVectorField; import org.apache.lucene.index.FieldInfo; import org.apache.lucene.index.FloatVectorValues; +import org.apache.lucene.index.LeafReader; import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.search.knn.KnnCollectorManager; import org.apache.lucene.util.ArrayUtil; @@ -84,24 +85,26 @@ public class KnnFloatVectorQuery extends AbstractKnnVectorQuery { KnnCollectorManager knnCollectorManager) throws IOException { KnnCollector knnCollector = knnCollectorManager.newCollector(visitedLimit, context); - FloatVectorValues floatVectorValues = context.reader().getFloatVectorValues(field); + LeafReader reader = context.reader(); + FloatVectorValues floatVectorValues = reader.getFloatVectorValues(field); if (floatVectorValues == null) { - FloatVectorValues.checkField(context.reader(), field); + FloatVectorValues.checkField(reader, field); return NO_RESULTS; } if (Math.min(knnCollector.k(), floatVectorValues.size()) == 0) { return NO_RESULTS; } - context.reader().searchNearestVectors(field, target, knnCollector, acceptDocs); + reader.searchNearestVectors(field, target, knnCollector, acceptDocs); TopDocs results = knnCollector.topDocs(); return results != null ? results : NO_RESULTS; } @Override VectorScorer createVectorScorer(LeafReaderContext context, FieldInfo fi) throws IOException { - FloatVectorValues vectorValues = context.reader().getFloatVectorValues(field); + LeafReader reader = context.reader(); + FloatVectorValues vectorValues = reader.getFloatVectorValues(field); if (vectorValues == null) { - FloatVectorValues.checkField(context.reader(), field); + FloatVectorValues.checkField(reader, field); return null; } return vectorValues.scorer(target); diff --git a/lucene/core/src/test/org/apache/lucene/search/TestKnnFloatVectorQuery.java b/lucene/core/src/test/org/apache/lucene/search/TestKnnFloatVectorQuery.java index 6ecc758c0e4..f2e5a3e274a 100644 --- a/lucene/core/src/test/org/apache/lucene/search/TestKnnFloatVectorQuery.java +++ b/lucene/core/src/test/org/apache/lucene/search/TestKnnFloatVectorQuery.java @@ -216,7 +216,7 @@ public class TestKnnFloatVectorQuery extends BaseKnnVectorQueryTestCase { maxScore = Math.max(maxScore, scores[i]); } IndexReader indexReader = searcher.getIndexReader(); - int[] segments = AbstractKnnVectorQuery.findSegmentStarts(indexReader, docs); + int[] segments = AbstractKnnVectorQuery.findSegmentStarts(indexReader.leaves(), docs); AbstractKnnVectorQuery.DocAndScoreQuery query = new AbstractKnnVectorQuery.DocAndScoreQuery( From f4cd4b46fc1ccb497624c5c5454785078aca6501 Mon Sep 17 00:00:00 2001 From: Christine Poerschke Date: Mon, 1 Jul 2024 17:32:04 +0100 Subject: [PATCH 27/42] Lucene99HnswVectorsReader.search float-vs-byte variants: reduce code duplication (#13529) * Lucene99HnswVectorsReader.search float-vs-byte variants: reduce code duplication * action review feedback: use org.apache.lucene.util.IOSupplier --- .../lucene99/Lucene99HnswVectorsReader.java | 53 +++++++++---------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99HnswVectorsReader.java b/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99HnswVectorsReader.java index 38908efbe68..899140af93a 100644 --- a/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99HnswVectorsReader.java +++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99HnswVectorsReader.java @@ -45,6 +45,7 @@ import org.apache.lucene.store.IndexInput; import org.apache.lucene.store.RandomAccessInput; import org.apache.lucene.store.ReadAdvice; import org.apache.lucene.util.Bits; +import org.apache.lucene.util.IOSupplier; import org.apache.lucene.util.IOUtils; import org.apache.lucene.util.RamUsageEstimator; import org.apache.lucene.util.hnsw.HnswGraph; @@ -248,45 +249,39 @@ public final class Lucene99HnswVectorsReader extends KnnVectorsReader @Override public void search(String field, float[] target, KnnCollector knnCollector, Bits acceptDocs) throws IOException { - FieldEntry fieldEntry = fields.get(field); - - if (fieldEntry.size() == 0 - || knnCollector.k() == 0 - || fieldEntry.vectorEncoding != VectorEncoding.FLOAT32) { - return; - } - final RandomVectorScorer scorer = flatVectorsReader.getRandomVectorScorer(field, target); - final KnnCollector collector = - new OrdinalTranslatedKnnCollector(knnCollector, scorer::ordToDoc); - final Bits acceptedOrds = scorer.getAcceptOrds(acceptDocs); - if (knnCollector.k() < scorer.maxOrd()) { - HnswGraphSearcher.search(scorer, collector, getGraph(fieldEntry), acceptedOrds); - } else { - // if k is larger than the number of vectors, we can just iterate over all vectors - // and collect them - for (int i = 0; i < scorer.maxOrd(); i++) { - if (acceptedOrds == null || acceptedOrds.get(i)) { - if (knnCollector.earlyTerminated()) { - break; - } - knnCollector.incVisitedCount(1); - knnCollector.collect(scorer.ordToDoc(i), scorer.score(i)); - } - } - } + search( + fields.get(field), + knnCollector, + acceptDocs, + VectorEncoding.FLOAT32, + () -> flatVectorsReader.getRandomVectorScorer(field, target)); } @Override public void search(String field, byte[] target, KnnCollector knnCollector, Bits acceptDocs) throws IOException { - FieldEntry fieldEntry = fields.get(field); + search( + fields.get(field), + knnCollector, + acceptDocs, + VectorEncoding.BYTE, + () -> flatVectorsReader.getRandomVectorScorer(field, target)); + } + + private void search( + FieldEntry fieldEntry, + KnnCollector knnCollector, + Bits acceptDocs, + VectorEncoding vectorEncoding, + IOSupplier scorerSupplier) + throws IOException { if (fieldEntry.size() == 0 || knnCollector.k() == 0 - || fieldEntry.vectorEncoding != VectorEncoding.BYTE) { + || fieldEntry.vectorEncoding != vectorEncoding) { return; } - final RandomVectorScorer scorer = flatVectorsReader.getRandomVectorScorer(field, target); + final RandomVectorScorer scorer = scorerSupplier.get(); final KnnCollector collector = new OrdinalTranslatedKnnCollector(knnCollector, scorer::ordToDoc); final Bits acceptedOrds = scorer.getAcceptOrds(acceptDocs); From 62e08f5f4b76b84ed4109d8493d1dac2048d3350 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Thu, 4 Jul 2024 11:15:26 +0200 Subject: [PATCH 28/42] TaskExecutor should not fork unnecessarily (#13472) When an executor is provided to the IndexSearcher constructor, the searcher now executes tasks on the thread that invoked a search as well as its configured executor. Users should reduce the executor's thread-count by 1 to retain the previous level of parallelism. Moreover, it is now possible to start searches from the same executor that is configured in the IndexSearcher without risk of deadlocking. A separate executor for starting searches is no longer required. Previously, a separate executor was required to prevent deadlock, and all the tasks were offloaded to it unconditionally, wasting resources in some scenarios due to unnecessary forking, and the caller thread having to wait for all tasks to be completed anyways. it can now actively contribute to the execution as well. --- lucene/CHANGES.txt | 9 ++ .../apache/lucene/search/TaskExecutor.java | 85 +++++++++---------- .../lucene/search/TestIndexSearcher.java | 4 +- .../lucene/search/TestTaskExecutor.java | 56 ++++++++---- 4 files changed, 90 insertions(+), 64 deletions(-) diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 320de7aaefe..74ff491b781 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -277,6 +277,15 @@ Optimizations * GITHUB#12941: Don't preserve auxiliary buffer contents in LSBRadixSorter if it grows. (Stefan Vodita) +Changes in runtime behavior +--------------------- + +* GITHUB#13472: When an executor is provided to the IndexSearcher constructor, the searcher now executes tasks on the + thread that invoked a search as well as its configured executor. Users should reduce the executor's thread-count by 1 + to retain the previous level of parallelism. Moreover, it is now possible to start searches from the same executor + that is configured in the IndexSearcher without risk of deadlocking. A separate executor for starting searches is no + longer required. (Armin Braun) + Bug Fixes --------------------- diff --git a/lucene/core/src/java/org/apache/lucene/search/TaskExecutor.java b/lucene/core/src/java/org/apache/lucene/search/TaskExecutor.java index 2763a9800e8..8832cf478e9 100644 --- a/lucene/core/src/java/org/apache/lucene/search/TaskExecutor.java +++ b/lucene/core/src/java/org/apache/lucene/search/TaskExecutor.java @@ -30,6 +30,7 @@ import java.util.concurrent.Future; import java.util.concurrent.FutureTask; import java.util.concurrent.RunnableFuture; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import org.apache.lucene.util.IOUtils; import org.apache.lucene.util.ThreadInterruptedException; @@ -37,20 +38,13 @@ import org.apache.lucene.util.ThreadInterruptedException; * Executor wrapper responsible for the execution of concurrent tasks. Used to parallelize search * across segments as well as query rewrite in some cases. Exposes a single {@link * #invokeAll(Collection)} method that takes a collection of {@link Callable}s and executes them - * concurrently/ Once all tasks are submitted to the executor, it blocks and wait for all tasks to - * be completed, and then returns a list with the obtained results. Ensures that the underlying - * executor is only used for top-level {@link #invokeAll(Collection)} calls, and not for potential - * {@link #invokeAll(Collection)} calls made from one of the tasks. This is to prevent deadlock with - * certain types of pool based executors (e.g. {@link java.util.concurrent.ThreadPoolExecutor}). + * concurrently. Once all but one task have been submitted to the executor, it tries to run as many + * tasks as possible on the calling thread, then waits for all tasks that have been executed in + * parallel on the executor to be completed and then returns a list with the obtained results. * * @lucene.experimental */ public final class TaskExecutor { - // a static thread local is ok as long as we use a counter, which accounts for multiple - // searchers holding a different TaskExecutor all backed by the same executor - private static final ThreadLocal numberOfRunningTasksInCurrentThread = - ThreadLocal.withInitial(() -> 0); - private final Executor executor; /** @@ -84,26 +78,21 @@ public final class TaskExecutor { /** * Holds all the sub-tasks that a certain operation gets split into as it gets parallelized and * exposes the ability to invoke such tasks and wait for them all to complete their execution and - * provide their results. Ensures that each task does not get parallelized further: this is - * important to avoid a deadlock in situations where one executor thread waits on other executor - * threads to complete before it can progress. This happens in situations where for instance - * {@link Query#createWeight(IndexSearcher, ScoreMode, float)} is called as part of searching each - * slice, like {@link TopFieldCollector#populateScores(ScoreDoc[], IndexSearcher, Query)} does. - * Additionally, if one task throws an exception, all other tasks from the same group are - * cancelled, to avoid needless computation as their results would not be exposed anyways. Creates - * one {@link FutureTask} for each {@link Callable} provided + * provide their results. Additionally, if one task throws an exception, all other tasks from the + * same group are cancelled, to avoid needless computation as their results would not be exposed + * anyways. Creates one {@link FutureTask} for each {@link Callable} provided * * @param the return type of all the callables */ private static final class TaskGroup { - private final Collection> futures; + private final List> futures; TaskGroup(Collection> callables) { List> tasks = new ArrayList<>(callables.size()); for (Callable callable : callables) { tasks.add(createTask(callable)); } - this.futures = Collections.unmodifiableCollection(tasks); + this.futures = Collections.unmodifiableList(tasks); } RunnableFuture createTask(Callable callable) { @@ -112,15 +101,10 @@ public final class TaskExecutor { () -> { if (startedOrCancelled.compareAndSet(false, true)) { try { - Integer counter = numberOfRunningTasksInCurrentThread.get(); - numberOfRunningTasksInCurrentThread.set(counter + 1); return callable.call(); } catch (Throwable t) { cancelAll(); throw t; - } finally { - Integer counter = numberOfRunningTasksInCurrentThread.get(); - numberOfRunningTasksInCurrentThread.set(counter - 1); } } // task is cancelled hence it has no results to return. That's fine: they would be @@ -144,32 +128,45 @@ public final class TaskExecutor { } List invokeAll(Executor executor) throws IOException { - boolean runOnCallerThread = numberOfRunningTasksInCurrentThread.get() > 0; - for (Runnable runnable : futures) { - if (runOnCallerThread) { - runnable.run(); - } else { - executor.execute(runnable); + final int count = futures.size(); + // taskId provides the first index of an un-executed task in #futures + final AtomicInteger taskId = new AtomicInteger(0); + // we fork execution count - 1 tasks to execute at least one task on the current thread to + // minimize needless forking and blocking of the current thread + if (count > 1) { + final Runnable work = + () -> { + int id = taskId.getAndIncrement(); + if (id < count) { + futures.get(id).run(); + } + }; + for (int j = 0; j < count - 1; j++) { + executor.execute(work); + } + } + // try to execute as many tasks as possible on the current thread to minimize context + // switching in case of long running concurrent + // tasks as well as dead-locking if the current thread is part of #executor for executors that + // have limited or no parallelism + int id; + while ((id = taskId.getAndIncrement()) < count) { + futures.get(id).run(); + if (id >= count - 1) { + // save redundant CAS in case this was the last task + break; } } Throwable exc = null; - List results = new ArrayList<>(futures.size()); - for (Future future : futures) { + List results = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + Future future = futures.get(i); try { results.add(future.get()); } catch (InterruptedException e) { - var newException = new ThreadInterruptedException(e); - if (exc == null) { - exc = newException; - } else { - exc.addSuppressed(newException); - } + exc = IOUtils.useOrSuppress(exc, new ThreadInterruptedException(e)); } catch (ExecutionException e) { - if (exc == null) { - exc = e.getCause(); - } else { - exc.addSuppressed(e.getCause()); - } + exc = IOUtils.useOrSuppress(exc, e.getCause()); } } assert assertAllFuturesCompleted() : "Some tasks are still running?"; diff --git a/lucene/core/src/test/org/apache/lucene/search/TestIndexSearcher.java b/lucene/core/src/test/org/apache/lucene/search/TestIndexSearcher.java index 3cd2ecbdd19..724013abac7 100644 --- a/lucene/core/src/test/org/apache/lucene/search/TestIndexSearcher.java +++ b/lucene/core/src/test/org/apache/lucene/search/TestIndexSearcher.java @@ -266,7 +266,7 @@ public class TestIndexSearcher extends LuceneTestCase { IOUtils.close(r, dir); } - public void testSlicesAllOffloadedToTheExecutor() throws IOException { + public void testSlicesOffloadedToTheExecutor() throws IOException { List leaves = reader.leaves(); AtomicInteger numExecutions = new AtomicInteger(0); IndexSearcher searcher = @@ -286,7 +286,7 @@ public class TestIndexSearcher extends LuceneTestCase { } }; searcher.search(new MatchAllDocsQuery(), 10); - assertEquals(leaves.size(), numExecutions.get()); + assertEquals(leaves.size() - 1, numExecutions.get()); } public void testNullExecutorNonNullTaskExecutor() { diff --git a/lucene/core/src/test/org/apache/lucene/search/TestTaskExecutor.java b/lucene/core/src/test/org/apache/lucene/search/TestTaskExecutor.java index ba5fa90f227..1949afbd51c 100644 --- a/lucene/core/src/test/org/apache/lucene/search/TestTaskExecutor.java +++ b/lucene/core/src/test/org/apache/lucene/search/TestTaskExecutor.java @@ -21,10 +21,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import org.apache.lucene.document.Document; import org.apache.lucene.index.DirectoryReader; @@ -111,7 +108,20 @@ public class TestTaskExecutor extends LuceneTestCase { assertEquals("exc", runtimeException.getCause().getMessage()); } - public void testInvokeAllFromTaskDoesNotDeadlockSameSearcher() throws IOException { + public void testInvokeAllFromTaskDoesNotDeadlockSameSearcher() throws Exception { + doTestInvokeAllFromTaskDoesNotDeadlockSameSearcher(executorService); + doTestInvokeAllFromTaskDoesNotDeadlockSameSearcher(Runnable::run); + executorService + .submit( + () -> { + doTestInvokeAllFromTaskDoesNotDeadlockSameSearcher(executorService); + return null; + }) + .get(); + } + + private static void doTestInvokeAllFromTaskDoesNotDeadlockSameSearcher(Executor executor) + throws IOException { try (Directory dir = newDirectory(); RandomIndexWriter iw = new RandomIndexWriter(random(), dir)) { for (int i = 0; i < 500; i++) { @@ -119,7 +129,7 @@ public class TestTaskExecutor extends LuceneTestCase { } try (DirectoryReader reader = iw.getReader()) { IndexSearcher searcher = - new IndexSearcher(reader, executorService) { + new IndexSearcher(reader, executor) { @Override protected LeafSlice[] slices(List leaves) { return slices(leaves, 1, 1); @@ -172,7 +182,20 @@ public class TestTaskExecutor extends LuceneTestCase { } } - public void testInvokeAllFromTaskDoesNotDeadlockMultipleSearchers() throws IOException { + public void testInvokeAllFromTaskDoesNotDeadlockMultipleSearchers() throws Exception { + doTestInvokeAllFromTaskDoesNotDeadlockMultipleSearchers(executorService); + doTestInvokeAllFromTaskDoesNotDeadlockMultipleSearchers(Runnable::run); + executorService + .submit( + () -> { + doTestInvokeAllFromTaskDoesNotDeadlockMultipleSearchers(executorService); + return null; + }) + .get(); + } + + private static void doTestInvokeAllFromTaskDoesNotDeadlockMultipleSearchers(Executor executor) + throws IOException { try (Directory dir = newDirectory(); RandomIndexWriter iw = new RandomIndexWriter(random(), dir)) { for (int i = 0; i < 500; i++) { @@ -180,7 +203,7 @@ public class TestTaskExecutor extends LuceneTestCase { } try (DirectoryReader reader = iw.getReader()) { IndexSearcher searcher = - new IndexSearcher(reader, executorService) { + new IndexSearcher(reader, executor) { @Override protected LeafSlice[] slices(List leaves) { return slices(leaves, 1, 1); @@ -202,7 +225,7 @@ public class TestTaskExecutor extends LuceneTestCase { // searcher has its own // TaskExecutor, the safeguard is shared among all the searchers that get // the same executor - IndexSearcher indexSearcher = new IndexSearcher(reader, executorService); + IndexSearcher indexSearcher = new IndexSearcher(reader, executor); indexSearcher .getTaskExecutor() .invokeAll(Collections.singletonList(() -> null)); @@ -234,11 +257,8 @@ public class TestTaskExecutor extends LuceneTestCase { TaskExecutor taskExecutor = new TaskExecutor( command -> { - executorService.execute( - () -> { - tasksStarted.incrementAndGet(); - command.run(); - }); + tasksStarted.incrementAndGet(); + command.run(); }); AtomicInteger tasksExecuted = new AtomicInteger(0); List> callables = new ArrayList<>(); @@ -251,14 +271,14 @@ public class TestTaskExecutor extends LuceneTestCase { for (int i = 0; i < tasksWithNormalExit; i++) { callables.add( () -> { - tasksExecuted.incrementAndGet(); - return null; + throw new AssertionError( + "must not be called since the first task failing cancels all subsequent tasks"); }); } expectThrows(RuntimeException.class, () -> taskExecutor.invokeAll(callables)); assertEquals(1, tasksExecuted.get()); // the callables are technically all run, but the cancelled ones will be no-op - assertEquals(100, tasksStarted.get()); + assertEquals(tasksWithNormalExit, tasksStarted.get()); } /** @@ -308,7 +328,7 @@ public class TestTaskExecutor extends LuceneTestCase { } public void testCancelTasksOnException() { - TaskExecutor taskExecutor = new TaskExecutor(executorService); + TaskExecutor taskExecutor = new TaskExecutor(Runnable::run); final int numTasks = random().nextInt(10, 50); final int throwingTask = random().nextInt(numTasks); boolean error = random().nextBoolean(); From 2a8d328ab22261d22616370b51da088aa005223f Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Mon, 8 Jul 2024 10:52:28 +0200 Subject: [PATCH 29/42] Replace AtomicLong with LongAdder in HitsThresholdChecker (#13546) The value for the global count is incremented a lot more than it is read, the space overhead of LongAdder seems irrelevant => lets use LongAdder. The performance gain from using it is the higher the more threads you use, but at 4 threads already very visible in benchmarks. --- .../org/apache/lucene/search/HitsThresholdChecker.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lucene/core/src/java/org/apache/lucene/search/HitsThresholdChecker.java b/lucene/core/src/java/org/apache/lucene/search/HitsThresholdChecker.java index fb6f6bf4343..4232af53fe6 100644 --- a/lucene/core/src/java/org/apache/lucene/search/HitsThresholdChecker.java +++ b/lucene/core/src/java/org/apache/lucene/search/HitsThresholdChecker.java @@ -17,13 +17,13 @@ package org.apache.lucene.search; -import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.LongAdder; /** Used for defining custom algorithms to allow searches to early terminate */ abstract class HitsThresholdChecker { /** Implementation of HitsThresholdChecker which allows global hit counting */ private static class GlobalHitsThresholdChecker extends HitsThresholdChecker { - private final AtomicLong globalHitCount = new AtomicLong(); + private final LongAdder globalHitCount = new LongAdder(); GlobalHitsThresholdChecker(int totalHitsThreshold) { super(totalHitsThreshold); @@ -32,12 +32,12 @@ abstract class HitsThresholdChecker { @Override void incrementHitCount() { - globalHitCount.incrementAndGet(); + globalHitCount.increment(); } @Override boolean isThresholdReached() { - return globalHitCount.getAcquire() > getHitsThreshold(); + return globalHitCount.longValue() > getHitsThreshold(); } @Override From 675772546c5419f32fc70163c10f999d86b8a89c Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Mon, 8 Jul 2024 10:53:26 +0200 Subject: [PATCH 30/42] Optimize MaxScoreBulkScorer (#13544) Don't use Comparator.comparingDouble(...) in a hotish loop here, it causes allocations that escape analysis is not able to remove. => lets just manually inline this to get predictable behavior and save up to 0.5% of all allocations in some benchmark runs. --- .../org/apache/lucene/search/MaxScoreBulkScorer.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lucene/core/src/java/org/apache/lucene/search/MaxScoreBulkScorer.java b/lucene/core/src/java/org/apache/lucene/search/MaxScoreBulkScorer.java index 026bf1f7d53..f250abfbcb9 100644 --- a/lucene/core/src/java/org/apache/lucene/search/MaxScoreBulkScorer.java +++ b/lucene/core/src/java/org/apache/lucene/search/MaxScoreBulkScorer.java @@ -18,7 +18,6 @@ package org.apache.lucene.search; import java.io.IOException; import java.util.Arrays; -import java.util.Comparator; import java.util.List; import org.apache.lucene.util.Bits; import org.apache.lucene.util.FixedBitSet; @@ -43,7 +42,7 @@ final class MaxScoreBulkScorer extends BulkScorer { int firstRequiredScorer; private final long cost; float minCompetitiveScore; - private Score scorable = new Score(); + private final Score scorable = new Score(); final double[] maxScoreSums; private final long[] windowMatches = new long[FixedBitSet.bits2words(INNER_WINDOW_SIZE)]; @@ -333,10 +332,14 @@ final class MaxScoreBulkScorer extends BulkScorer { // make a difference when using custom scores (like FuzzyQuery), high query-time boosts, or // scoring based on wacky weights. System.arraycopy(allScorers, 0, scratch, 0, allScorers.length); + // Do not use Comparator#comparingDouble below, it might cause unnecessary allocations Arrays.sort( scratch, - Comparator.comparingDouble( - scorer -> (double) scorer.maxWindowScore / Math.max(1L, scorer.cost))); + (scorer1, scorer2) -> { + return Double.compare( + (double) scorer1.maxWindowScore / Math.max(1L, scorer1.cost), + (double) scorer2.maxWindowScore / Math.max(1L, scorer2.cost)); + }); double maxScoreSum = 0; firstEssentialScorer = 0; for (int i = 0; i < allScorers.length; ++i) { From 9e04cb9c41bcfb43bb1e8d1bc977ef7ec527aff9 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Mon, 8 Jul 2024 10:59:50 +0200 Subject: [PATCH 31/42] Override single byte writes to OutputStreamIndexOutput to remove locking (#13543) Single byte writes to BufferedOutputStream show up pretty hot in indexing benchmarks. We can save the locking overhead introduced by JEP374 by overriding and providing a no-lock fastpath. --- .../lucene/store/OutputStreamIndexOutput.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lucene/core/src/java/org/apache/lucene/store/OutputStreamIndexOutput.java b/lucene/core/src/java/org/apache/lucene/store/OutputStreamIndexOutput.java index 5c3b4dbd88a..14b56a67e8a 100644 --- a/lucene/core/src/java/org/apache/lucene/store/OutputStreamIndexOutput.java +++ b/lucene/core/src/java/org/apache/lucene/store/OutputStreamIndexOutput.java @@ -135,5 +135,19 @@ public class OutputStreamIndexOutput extends IndexOutput { BitUtil.VH_LE_LONG.set(buf, count, i); count += Long.BYTES; } + + @Override + public void write(int b) throws IOException { + // override single byte write to avoid synchronization overhead now that JEP374 removed biased + // locking + byte[] buffer = buf; + int count = this.count; + if (count >= buffer.length) { + super.write(b); + } else { + buffer[count] = (byte) b; + this.count = count + 1; + } + } } } From 3304b60c9cc2fd23531263ea9d95dc3e92c0b2ce Mon Sep 17 00:00:00 2001 From: Chris Hegarty <62058229+ChrisHegarty@users.noreply.github.com> Date: Mon, 8 Jul 2024 17:30:45 +0100 Subject: [PATCH 32/42] Improve VectorUtil::xorBitCount perf on ARM (#13545) This commit improves the performance of VectorUtil::xorBitCount on ARM by ~4x. This change is effectively a workaround for the lack of vectorization of Long::bitCount on ARM. On x64 there is no issue, the long variant of xorBitCount outperforms the int variant by ~15%. --- .../jmh/HammingDistanceBenchmark.java | 75 ++++++++++++++++++ .../org/apache/lucene/util/VectorUtil.java | 34 +++++++- .../apache/lucene/util/TestVectorUtil.java | 77 +++++++++++++++++++ 3 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/HammingDistanceBenchmark.java diff --git a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/HammingDistanceBenchmark.java b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/HammingDistanceBenchmark.java new file mode 100644 index 00000000000..943d5961820 --- /dev/null +++ b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/HammingDistanceBenchmark.java @@ -0,0 +1,75 @@ +/* + * 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.benchmark.jmh; + +import java.io.IOException; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import org.apache.lucene.util.VectorUtil; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +@Fork(1) +@Warmup(iterations = 3, time = 3) +@Measurement(iterations = 5, time = 3) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Benchmark) +public class HammingDistanceBenchmark { + @Param({"1000000"}) + int nb = 1_000_000; + + @Param({"1024"}) + int dims = 1024; + + byte[][] xb; + byte[] xq; + + @Setup + public void setup() throws IOException { + Random rand = new Random(); + this.xb = new byte[nb][dims / 8]; + for (int i = 0; i < nb; i++) { + for (int j = 0; j < dims / 8; j++) { + xb[i][j] = (byte) rand.nextInt(0, 255); + } + } + this.xq = new byte[dims / 8]; + for (int i = 0; i < xq.length; i++) { + xq[i] = (byte) rand.nextInt(0, 255); + } + } + + @Benchmark + public int xorBitCount() { + int tot = 0; + for (int i = 0; i < nb; i++) { + tot += VectorUtil.xorBitCount(xb[i], xq); + } + return tot; + } +} diff --git a/lucene/core/src/java/org/apache/lucene/util/VectorUtil.java b/lucene/core/src/java/org/apache/lucene/util/VectorUtil.java index 43e5077695c..5f4deea6b72 100644 --- a/lucene/core/src/java/org/apache/lucene/util/VectorUtil.java +++ b/lucene/core/src/java/org/apache/lucene/util/VectorUtil.java @@ -212,6 +212,14 @@ public final class VectorUtil { return IMPL.int4DotProduct(unpacked, false, packed, true); } + /** + * For xorBitCount we stride over the values as either 64-bits (long) or 32-bits (int) at a time. + * On ARM Long::bitCount is not vectorized, and therefore produces less than optimal code, when + * compared to Integer::bitCount. While Long::bitCount is optimal on x64. TODO: include the + * OpenJDK JIRA url + */ + static final boolean XOR_BIT_COUNT_STRIDE_AS_INT = Constants.OS_ARCH.equals("aarch64"); + /** * XOR bit count computed over signed bytes. * @@ -223,8 +231,32 @@ public final class VectorUtil { if (a.length != b.length) { throw new IllegalArgumentException("vector dimensions differ: " + a.length + "!=" + b.length); } + if (XOR_BIT_COUNT_STRIDE_AS_INT) { + return xorBitCountInt(a, b); + } else { + return xorBitCountLong(a, b); + } + } + + /** XOR bit count striding over 4 bytes at a time. */ + static int xorBitCountInt(byte[] a, byte[] b) { int distance = 0, i = 0; - for (final int upperBound = a.length & ~(Long.BYTES - 1); i < upperBound; i += Long.BYTES) { + for (final int upperBound = a.length & -Integer.BYTES; i < upperBound; i += Integer.BYTES) { + distance += + Integer.bitCount( + (int) BitUtil.VH_NATIVE_INT.get(a, i) ^ (int) BitUtil.VH_NATIVE_INT.get(b, i)); + } + // tail: + for (; i < a.length; i++) { + distance += Integer.bitCount((a[i] ^ b[i]) & 0xFF); + } + return distance; + } + + /** XOR bit count striding over 8 bytes at a time. */ + static int xorBitCountLong(byte[] a, byte[] b) { + int distance = 0, i = 0; + for (final int upperBound = a.length & -Long.BYTES; i < upperBound; i += Long.BYTES) { distance += Long.bitCount( (long) BitUtil.VH_NATIVE_LONG.get(a, i) ^ (long) BitUtil.VH_NATIVE_LONG.get(b, i)); diff --git a/lucene/core/src/test/org/apache/lucene/util/TestVectorUtil.java b/lucene/core/src/test/org/apache/lucene/util/TestVectorUtil.java index 3153f3992fb..00577c3db52 100644 --- a/lucene/core/src/test/org/apache/lucene/util/TestVectorUtil.java +++ b/lucene/core/src/test/org/apache/lucene/util/TestVectorUtil.java @@ -276,4 +276,81 @@ public class TestVectorUtil extends LuceneTestCase { u[1] = -v[0]; assertEquals(0, VectorUtil.cosine(u, v), DELTA); } + + interface ToIntBiFunction { + int apply(byte[] a, byte[] b); + } + + public void testBasicXorBitCount() { + testBasicXorBitCountImpl(VectorUtil::xorBitCount); + testBasicXorBitCountImpl(VectorUtil::xorBitCountInt); + testBasicXorBitCountImpl(VectorUtil::xorBitCountLong); + // test sanity + testBasicXorBitCountImpl(TestVectorUtil::xorBitCount); + } + + void testBasicXorBitCountImpl(ToIntBiFunction xorBitCount) { + assertEquals(0, xorBitCount.apply(new byte[] {1}, new byte[] {1})); + assertEquals(0, xorBitCount.apply(new byte[] {1, 2, 3}, new byte[] {1, 2, 3})); + assertEquals(1, xorBitCount.apply(new byte[] {1, 2, 3}, new byte[] {0, 2, 3})); + assertEquals(2, xorBitCount.apply(new byte[] {1, 2, 3}, new byte[] {0, 6, 3})); + assertEquals(3, xorBitCount.apply(new byte[] {1, 2, 3}, new byte[] {0, 6, 7})); + assertEquals(4, xorBitCount.apply(new byte[] {1, 2, 3}, new byte[] {2, 6, 7})); + + // 32-bit / int boundary + assertEquals(0, xorBitCount.apply(new byte[] {1, 2, 3, 4}, new byte[] {1, 2, 3, 4})); + assertEquals(1, xorBitCount.apply(new byte[] {1, 2, 3, 4}, new byte[] {0, 2, 3, 4})); + assertEquals(0, xorBitCount.apply(new byte[] {1, 2, 3, 4, 5}, new byte[] {1, 2, 3, 4, 5})); + assertEquals(1, xorBitCount.apply(new byte[] {1, 2, 3, 4, 5}, new byte[] {0, 2, 3, 4, 5})); + + // 64-bit / long boundary + assertEquals( + 0, + xorBitCount.apply( + new byte[] {1, 2, 3, 4, 5, 6, 7, 8}, new byte[] {1, 2, 3, 4, 5, 6, 7, 8})); + assertEquals( + 1, + xorBitCount.apply( + new byte[] {1, 2, 3, 4, 5, 6, 7, 8}, new byte[] {0, 2, 3, 4, 5, 6, 7, 8})); + + assertEquals( + 0, + xorBitCount.apply( + new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9}, new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9})); + assertEquals( + 1, + xorBitCount.apply( + new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9}, new byte[] {0, 2, 3, 4, 5, 6, 7, 8, 9})); + } + + public void testXorBitCount() { + int iterations = atLeast(100); + for (int i = 0; i < iterations; i++) { + int size = random().nextInt(1024); + byte[] a = new byte[size]; + byte[] b = new byte[size]; + random().nextBytes(a); + random().nextBytes(b); + + int expected = xorBitCount(a, b); + assertEquals(expected, VectorUtil.xorBitCount(a, b)); + assertEquals(expected, VectorUtil.xorBitCountInt(a, b)); + assertEquals(expected, VectorUtil.xorBitCountLong(a, b)); + } + } + + private static int xorBitCount(byte[] a, byte[] b) { + int res = 0; + for (int i = 0; i < a.length; i++) { + byte x = a[i]; + byte y = b[i]; + for (int j = 0; j < Byte.SIZE; j++) { + if (x == y) break; + if ((x & 0x01) != (y & 0x01)) res++; + x = (byte) ((x & 0xFF) >> 1); + y = (byte) ((y & 0xFF) >> 1); + } + } + return res; + } } From ceb4539609f087dc80936ee1a1640c9840ba2c30 Mon Sep 17 00:00:00 2001 From: Patrick Zhai Date: Mon, 8 Jul 2024 13:04:27 -0700 Subject: [PATCH 33/42] Refactor and javadoc update for KNN vector writer classes (#13548) --- lucene/CHANGES.txt | 3 +- .../lucene/codecs/KnnVectorsWriter.java | 58 +++++++++++++++++++ .../OrdToDocDISIReaderConfiguration.java | 2 +- .../lucene99/Lucene99FlatVectorsFormat.java | 4 +- .../lucene99/Lucene99FlatVectorsWriter.java | 24 +------- .../lucene99/Lucene99HnswVectorsFormat.java | 8 --- .../lucene99/Lucene99HnswVectorsWriter.java | 27 ++------- .../Lucene99ScalarQuantizedVectorsWriter.java | 23 +------- .../lucene/index/FieldUpdatesBuffer.java | 2 +- 9 files changed, 74 insertions(+), 77 deletions(-) diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 74ff491b781..1fa11f6625a 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -258,7 +258,8 @@ New Features Improvements --------------------- -(No changes) + +* GITHUB#13548: Refactor and javadoc update for KNN vector writer classes. (Patrick Zhai) Optimizations --------------------- diff --git a/lucene/core/src/java/org/apache/lucene/codecs/KnnVectorsWriter.java b/lucene/core/src/java/org/apache/lucene/codecs/KnnVectorsWriter.java index 8ae86d4c807..053ab893df1 100644 --- a/lucene/core/src/java/org/apache/lucene/codecs/KnnVectorsWriter.java +++ b/lucene/core/src/java/org/apache/lucene/codecs/KnnVectorsWriter.java @@ -20,14 +20,18 @@ package org.apache.lucene.codecs; import java.io.Closeable; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Objects; import org.apache.lucene.index.ByteVectorValues; import org.apache.lucene.index.DocIDMerger; +import org.apache.lucene.index.DocsWithFieldSet; import org.apache.lucene.index.FieldInfo; import org.apache.lucene.index.FloatVectorValues; import org.apache.lucene.index.MergeState; import org.apache.lucene.index.Sorter; import org.apache.lucene.index.VectorEncoding; +import org.apache.lucene.internal.hppc.IntIntHashMap; import org.apache.lucene.search.DocIdSetIterator; import org.apache.lucene.search.VectorScorer; import org.apache.lucene.util.Accountable; @@ -139,6 +143,60 @@ public abstract class KnnVectorsWriter implements Accountable, Closeable { } } + /** + * Given old doc ids and an id mapping, maps old ordinal to new ordinal. Note: this method return + * nothing and output are written to parameters + * + * @param oldDocIds the old or current document ordinals. Must not be null. + * @param sortMap the document sorting map for how to make the new ordinals. Must not be null. + * @param old2NewOrd int[] maps from old ord to new ord + * @param new2OldOrd int[] maps from new ord to old ord + * @param newDocsWithField set of new doc ids which has the value + */ + public static void mapOldOrdToNewOrd( + DocsWithFieldSet oldDocIds, + Sorter.DocMap sortMap, + int[] old2NewOrd, + int[] new2OldOrd, + DocsWithFieldSet newDocsWithField) + throws IOException { + // TODO: a similar function exists in IncrementalHnswGraphMerger#getNewOrdMapping + // maybe we can do a further refactoring + Objects.requireNonNull(oldDocIds); + Objects.requireNonNull(sortMap); + assert (old2NewOrd != null || new2OldOrd != null || newDocsWithField != null); + assert (old2NewOrd == null || old2NewOrd.length == oldDocIds.cardinality()); + assert (new2OldOrd == null || new2OldOrd.length == oldDocIds.cardinality()); + IntIntHashMap newIdToOldOrd = new IntIntHashMap(); + DocIdSetIterator iterator = oldDocIds.iterator(); + int[] newDocIds = new int[oldDocIds.cardinality()]; + int oldOrd = 0; + for (int oldDocId = iterator.nextDoc(); + oldDocId != DocIdSetIterator.NO_MORE_DOCS; + oldDocId = iterator.nextDoc()) { + int newId = sortMap.oldToNew(oldDocId); + newIdToOldOrd.put(newId, oldOrd); + newDocIds[oldOrd] = newId; + oldOrd++; + } + + Arrays.sort(newDocIds); + int newOrd = 0; + for (int newDocId : newDocIds) { + int currOldOrd = newIdToOldOrd.get(newDocId); + if (old2NewOrd != null) { + old2NewOrd[currOldOrd] = newOrd; + } + if (new2OldOrd != null) { + new2OldOrd[newOrd] = currOldOrd; + } + if (newDocsWithField != null) { + newDocsWithField.add(newDocId); + } + newOrd++; + } + } + /** View over multiple vector values supporting iterator-style access via DocIdMerger. */ public static final class MergedVectorValues { private MergedVectorValues() {} diff --git a/lucene/core/src/java/org/apache/lucene/codecs/lucene95/OrdToDocDISIReaderConfiguration.java b/lucene/core/src/java/org/apache/lucene/codecs/lucene95/OrdToDocDISIReaderConfiguration.java index a13485eed5d..e4c921ddee2 100644 --- a/lucene/core/src/java/org/apache/lucene/codecs/lucene95/OrdToDocDISIReaderConfiguration.java +++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene95/OrdToDocDISIReaderConfiguration.java @@ -40,7 +40,7 @@ public class OrdToDocDISIReaderConfiguration { *

Within outputMeta the format is as follows: * *

    - *
  • [int8] if equals to -2, empty - no vectory values. If equals to -1, dense – all + *
  • [int8] if equals to -2, empty - no vector values. If equals to -1, dense – all * documents have values for a field. If equals to 0, sparse – some documents missing * values. *
  • DocIds were encoded by {@link IndexedDISI#writeBitSet(DocIdSetIterator, IndexOutput, diff --git a/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99FlatVectorsFormat.java b/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99FlatVectorsFormat.java index 78e0cf000fa..c8ef2709db6 100644 --- a/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99FlatVectorsFormat.java +++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99FlatVectorsFormat.java @@ -56,8 +56,8 @@ import org.apache.lucene.store.IndexOutput; *
  • [vlong] length of this field's vectors, in bytes *
  • [vint] dimension of this field's vectors *
  • [int] the number of documents having values for this field - *
  • [int8] if equals to -1, dense – all documents have values for a field. If equals to - * 0, sparse – some documents missing values. + *
  • [int8] if equals to -2, empty - no vector values. If equals to -1, dense – all + * documents have values for a field. If equals to 0, sparse – some documents missing values. *
  • DocIds were encoded by {@link IndexedDISI#writeBitSet(DocIdSetIterator, IndexOutput, byte)} *
  • OrdToDoc was encoded by {@link org.apache.lucene.util.packed.DirectMonotonicWriter}, note * that only in sparse case diff --git a/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99FlatVectorsWriter.java b/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99FlatVectorsWriter.java index 6232489c08d..b80c9f4d7f5 100644 --- a/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99FlatVectorsWriter.java +++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99FlatVectorsWriter.java @@ -44,7 +44,6 @@ import org.apache.lucene.index.MergeState; import org.apache.lucene.index.SegmentWriteState; import org.apache.lucene.index.Sorter; import org.apache.lucene.index.VectorEncoding; -import org.apache.lucene.search.DocIdSetIterator; import org.apache.lucene.store.IOContext; import org.apache.lucene.store.IndexInput; import org.apache.lucene.store.IndexOutput; @@ -191,27 +190,10 @@ public final class Lucene99FlatVectorsWriter extends FlatVectorsWriter { private void writeSortingField(FieldWriter fieldData, int maxDoc, Sorter.DocMap sortMap) throws IOException { - final int[] docIdOffsets = new int[sortMap.size()]; - int offset = 1; // 0 means no vector for this (field, document) - DocIdSetIterator iterator = fieldData.docsWithField.iterator(); - for (int docID = iterator.nextDoc(); - docID != DocIdSetIterator.NO_MORE_DOCS; - docID = iterator.nextDoc()) { - int newDocID = sortMap.oldToNew(docID); - docIdOffsets[newDocID] = offset++; - } + final int[] ordMap = new int[fieldData.docsWithField.cardinality()]; // new ord to old ord + DocsWithFieldSet newDocsWithField = new DocsWithFieldSet(); - final int[] ordMap = new int[offset - 1]; // new ord to old ord - int ord = 0; - int doc = 0; - for (int docIdOffset : docIdOffsets) { - if (docIdOffset != 0) { - ordMap[ord] = docIdOffset - 1; - newDocsWithField.add(doc); - ord++; - } - doc++; - } + mapOldOrdToNewOrd(fieldData.docsWithField, sortMap, null, ordMap, newDocsWithField); // write vector values long vectorDataOffset = diff --git a/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99HnswVectorsFormat.java b/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99HnswVectorsFormat.java index 3238fd1f4ae..117393706db 100644 --- a/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99HnswVectorsFormat.java +++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99HnswVectorsFormat.java @@ -24,14 +24,11 @@ import org.apache.lucene.codecs.KnnVectorsReader; import org.apache.lucene.codecs.KnnVectorsWriter; import org.apache.lucene.codecs.hnsw.FlatVectorScorerUtil; import org.apache.lucene.codecs.hnsw.FlatVectorsFormat; -import org.apache.lucene.codecs.lucene90.IndexedDISI; import org.apache.lucene.index.MergePolicy; import org.apache.lucene.index.MergeScheduler; import org.apache.lucene.index.SegmentReadState; import org.apache.lucene.index.SegmentWriteState; -import org.apache.lucene.search.DocIdSetIterator; import org.apache.lucene.search.TaskExecutor; -import org.apache.lucene.store.IndexOutput; import org.apache.lucene.util.hnsw.HnswGraph; /** @@ -69,11 +66,6 @@ import org.apache.lucene.util.hnsw.HnswGraph; *
  • [vlong] length of this field's index data, in bytes *
  • [vint] dimension of this field's vectors *
  • [int] the number of documents having values for this field - *
  • [int8] if equals to -1, dense – all documents have values for a field. If equals to - * 0, sparse – some documents missing values. - *
  • DocIds were encoded by {@link IndexedDISI#writeBitSet(DocIdSetIterator, IndexOutput, byte)} - *
  • OrdToDoc was encoded by {@link org.apache.lucene.util.packed.DirectMonotonicWriter}, note - * that only in sparse case *
  • [vint] the maximum number of connections (neighbours) that each node can have *
  • [vint] number of levels in the graph *
  • Graph nodes by level. For each level diff --git a/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99HnswVectorsWriter.java b/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99HnswVectorsWriter.java index 8f715993a2b..e2171a3513f 100644 --- a/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99HnswVectorsWriter.java +++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99HnswVectorsWriter.java @@ -196,29 +196,10 @@ public final class Lucene99HnswVectorsWriter extends KnnVectorsWriter { private void writeSortingField(FieldWriter fieldData, Sorter.DocMap sortMap) throws IOException { - final int[] docIdOffsets = new int[sortMap.size()]; - int offset = 1; // 0 means no vector for this (field, document) - DocIdSetIterator iterator = fieldData.docsWithField.iterator(); - for (int docID = iterator.nextDoc(); - docID != DocIdSetIterator.NO_MORE_DOCS; - docID = iterator.nextDoc()) { - int newDocID = sortMap.oldToNew(docID); - docIdOffsets[newDocID] = offset++; - } - DocsWithFieldSet newDocsWithField = new DocsWithFieldSet(); - final int[] ordMap = new int[offset - 1]; // new ord to old ord - final int[] oldOrdMap = new int[offset - 1]; // old ord to new ord - int ord = 0; - int doc = 0; - for (int docIdOffset : docIdOffsets) { - if (docIdOffset != 0) { - ordMap[ord] = docIdOffset - 1; - oldOrdMap[docIdOffset - 1] = ord; - newDocsWithField.add(doc); - ord++; - } - doc++; - } + final int[] ordMap = new int[fieldData.docsWithField.cardinality()]; // new ord to old ord + final int[] oldOrdMap = new int[fieldData.docsWithField.cardinality()]; // old ord to new ord + + mapOldOrdToNewOrd(fieldData.docsWithField, sortMap, oldOrdMap, ordMap, null); // write graph long vectorIndexOffset = vectorIndex.getFilePointer(); OnHeapHnswGraph graph = fieldData.getGraph(); diff --git a/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99ScalarQuantizedVectorsWriter.java b/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99ScalarQuantizedVectorsWriter.java index c052ce20646..98dfd891ebf 100644 --- a/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99ScalarQuantizedVectorsWriter.java +++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99ScalarQuantizedVectorsWriter.java @@ -399,27 +399,10 @@ public final class Lucene99ScalarQuantizedVectorsWriter extends FlatVectorsWrite private void writeSortingField(FieldWriter fieldData, int maxDoc, Sorter.DocMap sortMap) throws IOException { - final int[] docIdOffsets = new int[sortMap.size()]; - int offset = 1; // 0 means no vector for this (field, document) - DocIdSetIterator iterator = fieldData.docsWithField.iterator(); - for (int docID = iterator.nextDoc(); - docID != DocIdSetIterator.NO_MORE_DOCS; - docID = iterator.nextDoc()) { - int newDocID = sortMap.oldToNew(docID); - docIdOffsets[newDocID] = offset++; - } + final int[] ordMap = new int[fieldData.docsWithField.cardinality()]; // new ord to old ord + DocsWithFieldSet newDocsWithField = new DocsWithFieldSet(); - final int[] ordMap = new int[offset - 1]; // new ord to old ord - int ord = 0; - int doc = 0; - for (int docIdOffset : docIdOffsets) { - if (docIdOffset != 0) { - ordMap[ord] = docIdOffset - 1; - newDocsWithField.add(doc); - ord++; - } - doc++; - } + mapOldOrdToNewOrd(fieldData.docsWithField, sortMap, null, ordMap, newDocsWithField); // write vector values long vectorDataOffset = quantizedVectorData.alignFilePointer(Float.BYTES); diff --git a/lucene/core/src/java/org/apache/lucene/index/FieldUpdatesBuffer.java b/lucene/core/src/java/org/apache/lucene/index/FieldUpdatesBuffer.java index 9739df9cd83..a4d38dcd203 100644 --- a/lucene/core/src/java/org/apache/lucene/index/FieldUpdatesBuffer.java +++ b/lucene/core/src/java/org/apache/lucene/index/FieldUpdatesBuffer.java @@ -356,7 +356,7 @@ final class FieldUpdatesBuffer { } } - BytesRef nextTerm() throws IOException { + private BytesRef nextTerm() throws IOException { if (lookAheadTermIterator != null) { if (bufferedUpdate.termValue == null) { lookAheadTermIterator.next(); From 4baaedaa67046adeed19cdc28a346c1ebc9e8b5d Mon Sep 17 00:00:00 2001 From: ChrisHegarty Date: Tue, 9 Jul 2024 12:17:21 +0100 Subject: [PATCH 34/42] Add link to OpenJDK JIRA issue for VectorUtil::xorBitCount --- lucene/core/src/java/org/apache/lucene/util/VectorUtil.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lucene/core/src/java/org/apache/lucene/util/VectorUtil.java b/lucene/core/src/java/org/apache/lucene/util/VectorUtil.java index 5f4deea6b72..0ae563c8701 100644 --- a/lucene/core/src/java/org/apache/lucene/util/VectorUtil.java +++ b/lucene/core/src/java/org/apache/lucene/util/VectorUtil.java @@ -215,8 +215,8 @@ public final class VectorUtil { /** * For xorBitCount we stride over the values as either 64-bits (long) or 32-bits (int) at a time. * On ARM Long::bitCount is not vectorized, and therefore produces less than optimal code, when - * compared to Integer::bitCount. While Long::bitCount is optimal on x64. TODO: include the - * OpenJDK JIRA url + * compared to Integer::bitCount. While Long::bitCount is optimal on x64. See + * https://bugs.openjdk.org/browse/JDK-8336000 */ static final boolean XOR_BIT_COUNT_STRIDE_AS_INT = Constants.OS_ARCH.equals("aarch64"); From 392ddc154f229e141ddbf32dde945b0fefe99b0e Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Tue, 9 Jul 2024 15:46:15 +0200 Subject: [PATCH 35/42] Introduce TestLucene90DocValuesFormatVariableSkipInterval for testing docvalues skipper index (#13550) this commit makes possible to configure dynamically the interval size for doc values skipperfor testing, and add a new test suite that changes the interval size randomly. --- .../lucene90/Lucene90DocValuesConsumer.java | 6 ++- .../lucene90/Lucene90DocValuesFormat.java | 17 +++++++-- ...90DocValuesFormatVariableSkipInterval.java | 38 +++++++++++++++++++ 3 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 lucene/core/src/test/org/apache/lucene/codecs/lucene90/TestLucene90DocValuesFormatVariableSkipInterval.java diff --git a/lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90DocValuesConsumer.java b/lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90DocValuesConsumer.java index 63e4891960c..021eacacd3b 100644 --- a/lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90DocValuesConsumer.java +++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90DocValuesConsumer.java @@ -19,7 +19,6 @@ package org.apache.lucene.codecs.lucene90; import static org.apache.lucene.codecs.lucene90.Lucene90DocValuesFormat.DIRECT_MONOTONIC_BLOCK_SHIFT; import static org.apache.lucene.codecs.lucene90.Lucene90DocValuesFormat.NUMERIC_BLOCK_SHIFT; import static org.apache.lucene.codecs.lucene90.Lucene90DocValuesFormat.NUMERIC_BLOCK_SIZE; -import static org.apache.lucene.codecs.lucene90.Lucene90DocValuesFormat.SKIP_INDEX_INTERVAL_SIZE; import java.io.IOException; import java.util.Arrays; @@ -63,10 +62,12 @@ final class Lucene90DocValuesConsumer extends DocValuesConsumer { IndexOutput data, meta; final int maxDoc; private byte[] termsDictBuffer; + private final int skipIndexIntervalSize; /** expert: Creates a new writer */ public Lucene90DocValuesConsumer( SegmentWriteState state, + int skipIndexIntervalSize, String dataCodec, String dataExtension, String metaCodec, @@ -96,6 +97,7 @@ final class Lucene90DocValuesConsumer extends DocValuesConsumer { state.segmentInfo.getId(), state.segmentSuffix); maxDoc = state.segmentInfo.maxDoc(); + this.skipIndexIntervalSize = skipIndexIntervalSize; success = true; } finally { if (!success) { @@ -239,7 +241,7 @@ final class Lucene90DocValuesConsumer extends DocValuesConsumer { for (int i = 0, end = values.docValueCount(); i < end; ++i) { accumulator.accumulate(values.nextValue()); } - if (++counter == SKIP_INDEX_INTERVAL_SIZE) { + if (++counter == skipIndexIntervalSize) { globalMaxValue = Math.max(globalMaxValue, accumulator.maxValue); globalMinValue = Math.min(globalMinValue, accumulator.minValue); globalDocCount += accumulator.docCount; diff --git a/lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90DocValuesFormat.java b/lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90DocValuesFormat.java index 847a5341584..0ae0a7ac2aa 100644 --- a/lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90DocValuesFormat.java +++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90DocValuesFormat.java @@ -138,15 +138,27 @@ import org.apache.lucene.util.packed.DirectWriter; */ public final class Lucene90DocValuesFormat extends DocValuesFormat { + private final int skipIndexIntervalSize; + /** Default constructor. */ public Lucene90DocValuesFormat() { + this(DEFAULT_SKIP_INDEX_INTERVAL_SIZE); + } + + /** Doc values fields format with specified skipIndexIntervalSize. */ + public Lucene90DocValuesFormat(int skipIndexIntervalSize) { super("Lucene90"); + if (skipIndexIntervalSize < 2) { + throw new IllegalArgumentException( + "skipIndexIntervalSize must be > 1, got [" + skipIndexIntervalSize + "]"); + } + this.skipIndexIntervalSize = skipIndexIntervalSize; } @Override public DocValuesConsumer fieldsConsumer(SegmentWriteState state) throws IOException { return new Lucene90DocValuesConsumer( - state, DATA_CODEC, DATA_EXTENSION, META_CODEC, META_EXTENSION); + state, skipIndexIntervalSize, DATA_CODEC, DATA_EXTENSION, META_CODEC, META_EXTENSION); } @Override @@ -182,6 +194,5 @@ public final class Lucene90DocValuesFormat extends DocValuesFormat { static final int TERMS_DICT_REVERSE_INDEX_SIZE = 1 << TERMS_DICT_REVERSE_INDEX_SHIFT; static final int TERMS_DICT_REVERSE_INDEX_MASK = TERMS_DICT_REVERSE_INDEX_SIZE - 1; - static final int SKIP_INDEX_INTERVAL_SHIFT = 12; - static final int SKIP_INDEX_INTERVAL_SIZE = 1 << SKIP_INDEX_INTERVAL_SHIFT; + private static final int DEFAULT_SKIP_INDEX_INTERVAL_SIZE = 4096; } diff --git a/lucene/core/src/test/org/apache/lucene/codecs/lucene90/TestLucene90DocValuesFormatVariableSkipInterval.java b/lucene/core/src/test/org/apache/lucene/codecs/lucene90/TestLucene90DocValuesFormatVariableSkipInterval.java new file mode 100644 index 00000000000..94204fb2a8d --- /dev/null +++ b/lucene/core/src/test/org/apache/lucene/codecs/lucene90/TestLucene90DocValuesFormatVariableSkipInterval.java @@ -0,0 +1,38 @@ +/* + * 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.codecs.lucene90; + +import org.apache.lucene.codecs.Codec; +import org.apache.lucene.tests.index.BaseDocValuesFormatTestCase; +import org.apache.lucene.tests.util.TestUtil; + +/** Tests Lucene90DocValuesFormat */ +public class TestLucene90DocValuesFormatVariableSkipInterval extends BaseDocValuesFormatTestCase { + + @Override + protected Codec getCodec() { + return TestUtil.alwaysDocValuesFormat(new Lucene90DocValuesFormat(random().nextInt(2, 1024))); + } + + public void testSkipIndexIntervalSize() { + IllegalArgumentException ex = + expectThrows( + IllegalArgumentException.class, + () -> new Lucene90DocValuesFormat(random().nextInt(Integer.MIN_VALUE, 2))); + assertTrue(ex.getMessage().contains("skipIndexIntervalSize must be > 1")); + } +} From 295c5d35767c2af9361f79dfc11511ad5ababd81 Mon Sep 17 00:00:00 2001 From: Jakub Slowinski <32519034+slow-J@users.noreply.github.com> Date: Tue, 9 Jul 2024 15:25:02 +0100 Subject: [PATCH 36/42] GITHUB#13175: Stop double-checking priority queue inserts in some FacetCount classes (#13488) * GITHUB#13175: Stop double-checking priority queue inserts Removing 2 cases of bottomX optimizations where insertWithOverflow already handles the check. Closes #13175 * Update CHANGES.txt --------- Co-authored-by: Jakub Slowinski --- lucene/CHANGES.txt | 2 + .../lucene/facet/StringValueFacetCounts.java | 48 +++++++------------ .../AbstractSortedSetDocValueFacetCounts.java | 28 ++++------- 3 files changed, 29 insertions(+), 49 deletions(-) diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 1fa11f6625a..16000083199 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -278,6 +278,8 @@ Optimizations * GITHUB#12941: Don't preserve auxiliary buffer contents in LSBRadixSorter if it grows. (Stefan Vodita) +* GITHUB#13175: Stop double-checking priority queue inserts in some FacetCount classes. (Jakub Slowinski) + Changes in runtime behavior --------------------- diff --git a/lucene/facet/src/java/org/apache/lucene/facet/StringValueFacetCounts.java b/lucene/facet/src/java/org/apache/lucene/facet/StringValueFacetCounts.java index 9e63043f3fa..655e80546f8 100644 --- a/lucene/facet/src/java/org/apache/lucene/facet/StringValueFacetCounts.java +++ b/lucene/facet/src/java/org/apache/lucene/facet/StringValueFacetCounts.java @@ -180,8 +180,6 @@ public class StringValueFacetCounts extends Facets { topN = Math.min(topN, cardinality); TopOrdAndIntQueue q = null; TopOrdAndIntQueue.OrdAndInt reuse = null; - int bottomCount = 0; - int bottomOrd = Integer.MAX_VALUE; int childCount = 0; // total number of labels with non-zero count if (sparseCounts != null) { @@ -189,7 +187,22 @@ public class StringValueFacetCounts extends Facets { childCount++; // every count in sparseValues should be non-zero int ord = sparseCount.key; int count = sparseCount.value; - if (count > bottomCount || (count == bottomCount && ord < bottomOrd)) { + if (q == null) { + // Lazy init for sparse case: + q = new TopOrdAndIntQueue(topN); + } + if (reuse == null) { + reuse = (TopOrdAndIntQueue.OrdAndInt) q.newOrdAndValue(); + } + reuse.ord = ord; + reuse.value = count; + reuse = (TopOrdAndIntQueue.OrdAndInt) q.insertWithOverflow(reuse); + } + } else if (denseCounts != null) { + for (int i = 0; i < denseCounts.length; i++) { + int count = denseCounts[i]; + if (count != 0) { + childCount++; if (q == null) { // Lazy init for sparse case: q = new TopOrdAndIntQueue(topN); @@ -197,36 +210,9 @@ public class StringValueFacetCounts extends Facets { if (reuse == null) { reuse = (TopOrdAndIntQueue.OrdAndInt) q.newOrdAndValue(); } - reuse.ord = ord; + reuse.ord = i; reuse.value = count; reuse = (TopOrdAndIntQueue.OrdAndInt) q.insertWithOverflow(reuse); - if (q.size() == topN) { - bottomCount = ((TopOrdAndIntQueue.OrdAndInt) q.top()).value; - bottomOrd = q.top().ord; - } - } - } - } else if (denseCounts != null) { - for (int i = 0; i < denseCounts.length; i++) { - int count = denseCounts[i]; - if (count != 0) { - childCount++; - if (count > bottomCount || (count == bottomCount && i < bottomOrd)) { - if (q == null) { - // Lazy init for sparse case: - q = new TopOrdAndIntQueue(topN); - } - if (reuse == null) { - reuse = (TopOrdAndIntQueue.OrdAndInt) q.newOrdAndValue(); - } - reuse.ord = i; - reuse.value = count; - reuse = (TopOrdAndIntQueue.OrdAndInt) q.insertWithOverflow(reuse); - if (q.size() == topN) { - bottomCount = ((TopOrdAndIntQueue.OrdAndInt) q.top()).value; - bottomOrd = q.top().ord; - } - } } } } diff --git a/lucene/facet/src/java/org/apache/lucene/facet/sortedset/AbstractSortedSetDocValueFacetCounts.java b/lucene/facet/src/java/org/apache/lucene/facet/sortedset/AbstractSortedSetDocValueFacetCounts.java index 230e9bf9e6c..03a0ce72190 100644 --- a/lucene/facet/src/java/org/apache/lucene/facet/sortedset/AbstractSortedSetDocValueFacetCounts.java +++ b/lucene/facet/src/java/org/apache/lucene/facet/sortedset/AbstractSortedSetDocValueFacetCounts.java @@ -322,8 +322,6 @@ abstract class AbstractSortedSetDocValueFacetCounts extends Facets { private TopChildrenForPath computeTopChildren( PrimitiveIterator.OfInt childOrds, int topN, DimConfig dimConfig, int pathOrd) { TopOrdAndIntQueue q = null; - int bottomCount = 0; - int bottomOrd = Integer.MAX_VALUE; int pathCount = 0; int childCount = 0; @@ -334,23 +332,17 @@ abstract class AbstractSortedSetDocValueFacetCounts extends Facets { if (count > 0) { pathCount += count; childCount++; - if (count > bottomCount || (count == bottomCount && ord < bottomOrd)) { - if (q == null) { - // Lazy init, so we don't create this for the - // sparse case unnecessarily - q = new TopOrdAndIntQueue(topN); - } - if (reuse == null) { - reuse = (TopOrdAndIntQueue.OrdAndInt) q.newOrdAndValue(); - } - reuse.ord = ord; - reuse.value = count; - reuse = (TopOrdAndIntQueue.OrdAndInt) q.insertWithOverflow(reuse); - if (q.size() == topN) { - bottomCount = ((TopOrdAndIntQueue.OrdAndInt) q.top()).value; - bottomOrd = q.top().ord; - } + if (q == null) { + // Lazy init, so we don't create this for the + // sparse case unnecessarily + q = new TopOrdAndIntQueue(topN); } + if (reuse == null) { + reuse = (TopOrdAndIntQueue.OrdAndInt) q.newOrdAndValue(); + } + reuse.ord = ord; + reuse.value = count; + reuse = (TopOrdAndIntQueue.OrdAndInt) q.insertWithOverflow(reuse); } } From 9bfde5514ca87a1171f5121c6ca8bf4f835d32be Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Tue, 9 Jul 2024 13:00:10 -0400 Subject: [PATCH 37/42] Fix quantized vector writer ram estimates (#13553) * Fix quantized vector writer ram estimates * add test & changes --- lucene/CHANGES.txt | 3 + .../lucene99/Lucene99HnswVectorsWriter.java | 4 +- .../Lucene99ScalarQuantizedVectorsWriter.java | 5 +- .../index/BaseKnnVectorsFormatTestCase.java | 76 +++++++++++++++++++ 4 files changed, 82 insertions(+), 6 deletions(-) diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 16000083199..b75eaa73ebf 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -297,6 +297,9 @@ Bug Fixes * GITHUB#13463: Address bug in MultiLeafKnnCollector causing #minCompetitiveSimilarity to stay artificially low in some corner cases. (Greg Miller) +* GITHUB#13553: Correct RamUsageEstimate for scalar quantized knn vector formats so that raw vectors are correctly + accounted for. (Ben Trent) + Other -------------------- (No changes) diff --git a/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99HnswVectorsWriter.java b/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99HnswVectorsWriter.java index e2171a3513f..949507848bf 100644 --- a/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99HnswVectorsWriter.java +++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99HnswVectorsWriter.java @@ -171,10 +171,8 @@ public final class Lucene99HnswVectorsWriter extends KnnVectorsWriter { @Override public long ramBytesUsed() { long total = SHALLOW_RAM_BYTES_USED; + // The vector delegate will also account for this writer's KnnFieldVectorsWriter objects total += flatVectorWriter.ramBytesUsed(); - for (FieldWriter field : fields) { - total += field.ramBytesUsed(); - } return total; } diff --git a/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99ScalarQuantizedVectorsWriter.java b/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99ScalarQuantizedVectorsWriter.java index 98dfd891ebf..beb1af19ca1 100644 --- a/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99ScalarQuantizedVectorsWriter.java +++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99ScalarQuantizedVectorsWriter.java @@ -299,9 +299,8 @@ public final class Lucene99ScalarQuantizedVectorsWriter extends FlatVectorsWrite @Override public long ramBytesUsed() { long total = SHALLOW_RAM_BYTES_USED; - for (FieldWriter field : fields) { - total += field.ramBytesUsed(); - } + // The vector delegate will also account for this writer's KnnFieldVectorsWriter objects + total += rawVectorDelegate.ramBytesUsed(); return total; } diff --git a/lucene/test-framework/src/java/org/apache/lucene/tests/index/BaseKnnVectorsFormatTestCase.java b/lucene/test-framework/src/java/org/apache/lucene/tests/index/BaseKnnVectorsFormatTestCase.java index 7a1f8b232c9..a10d2642349 100644 --- a/lucene/test-framework/src/java/org/apache/lucene/tests/index/BaseKnnVectorsFormatTestCase.java +++ b/lucene/test-framework/src/java/org/apache/lucene/tests/index/BaseKnnVectorsFormatTestCase.java @@ -23,10 +23,15 @@ import static org.apache.lucene.search.DocIdSetIterator.NO_MORE_DOCS; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import java.util.Set; import org.apache.lucene.codecs.Codec; +import org.apache.lucene.codecs.KnnFieldVectorsWriter; import org.apache.lucene.codecs.KnnVectorsFormat; import org.apache.lucene.codecs.KnnVectorsReader; +import org.apache.lucene.codecs.KnnVectorsWriter; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.KnnByteVectorField; @@ -38,13 +43,19 @@ import org.apache.lucene.index.ByteVectorValues; import org.apache.lucene.index.CheckIndex; import org.apache.lucene.index.CodecReader; import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.DocValuesType; +import org.apache.lucene.index.FieldInfo; +import org.apache.lucene.index.FieldInfos; import org.apache.lucene.index.FloatVectorValues; +import org.apache.lucene.index.IndexOptions; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.index.LeafReader; import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.index.NoMergePolicy; +import org.apache.lucene.index.SegmentInfo; +import org.apache.lucene.index.SegmentWriteState; import org.apache.lucene.index.StoredFields; import org.apache.lucene.index.Term; import org.apache.lucene.index.VectorEncoding; @@ -60,7 +71,10 @@ import org.apache.lucene.store.FSDirectory; import org.apache.lucene.tests.util.TestUtil; import org.apache.lucene.util.Bits; import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.InfoStream; +import org.apache.lucene.util.StringHelper; import org.apache.lucene.util.VectorUtil; +import org.apache.lucene.util.Version; import org.junit.Before; /** @@ -216,6 +230,68 @@ public abstract class BaseKnnVectorsFormatTestCase extends BaseIndexFileFormatTe } } + @SuppressWarnings("unchecked") + public void testWriterRamEstimate() throws Exception { + final FieldInfos fieldInfos = new FieldInfos(new FieldInfo[0]); + final Directory dir = newDirectory(); + Codec codec = Codec.getDefault(); + final SegmentInfo si = + new SegmentInfo( + dir, + Version.LATEST, + Version.LATEST, + "0", + 10000, + false, + false, + codec, + Collections.emptyMap(), + StringHelper.randomId(), + new HashMap<>(), + null); + final SegmentWriteState state = + new SegmentWriteState( + InfoStream.getDefault(), dir, si, fieldInfos, null, newIOContext(random())); + final KnnVectorsFormat format = codec.knnVectorsFormat(); + try (KnnVectorsWriter writer = format.fieldsWriter(state)) { + final long ramBytesUsed = writer.ramBytesUsed(); + int dim = random().nextInt(64) + 1; + if (dim % 2 == 1) { + ++dim; + } + int numDocs = atLeast(100); + KnnFieldVectorsWriter fieldWriter = + (KnnFieldVectorsWriter) + writer.addField( + new FieldInfo( + "fieldA", + 0, + false, + false, + false, + IndexOptions.NONE, + DocValuesType.NONE, + false, + -1, + Map.of(), + 0, + 0, + 0, + dim, + VectorEncoding.FLOAT32, + VectorSimilarityFunction.DOT_PRODUCT, + false, + false)); + for (int i = 0; i < numDocs; i++) { + fieldWriter.addValue(i, randomVector(dim)); + } + final long ramBytesUsed2 = writer.ramBytesUsed(); + assertTrue(ramBytesUsed2 > ramBytesUsed); + assertTrue(ramBytesUsed2 > (long) dim * numDocs * Float.BYTES); + } + dir.close(); + } + public void testIllegalSimilarityFunctionChangeTwoWriters() throws Exception { try (Directory dir = newDirectory()) { try (IndexWriter w = new IndexWriter(dir, newIndexWriterConfig())) { From ef215d87abadde1ab126ea77f5e266b280c68000 Mon Sep 17 00:00:00 2001 From: zhouhui Date: Wed, 10 Jul 2024 16:14:14 +0800 Subject: [PATCH 38/42] Lookup next when current doc is deleted in PerThreadPKLookup.lookup (#13556) --- .../lucene/tests/index/PerThreadPKLookup.java | 11 +-- .../tests/search/TestPerThreadPKLookup.java | 70 +++++++++++++++++++ 2 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 lucene/test-framework/src/test/org/apache/lucene/tests/search/TestPerThreadPKLookup.java diff --git a/lucene/test-framework/src/java/org/apache/lucene/tests/index/PerThreadPKLookup.java b/lucene/test-framework/src/java/org/apache/lucene/tests/index/PerThreadPKLookup.java index b5327515abd..c1823e8b708 100644 --- a/lucene/test-framework/src/java/org/apache/lucene/tests/index/PerThreadPKLookup.java +++ b/lucene/test-framework/src/java/org/apache/lucene/tests/index/PerThreadPKLookup.java @@ -84,10 +84,13 @@ public class PerThreadPKLookup { for (int seg = 0; seg < numSegs; seg++) { if (termsEnums[seg].seekExact(id)) { postingsEnums[seg] = termsEnums[seg].postings(postingsEnums[seg], 0); - int docID = postingsEnums[seg].nextDoc(); - if (docID != PostingsEnum.NO_MORE_DOCS - && (liveDocs[seg] == null || liveDocs[seg].get(docID))) { - return docBases[seg] + docID; + int docID = -1; + // TODO: Can we get postings' last Doc directly? and return the last one we find. + // TODO: Maybe we should check liveDoc whether null out of the loop? + while ((docID = postingsEnums[seg].nextDoc()) != PostingsEnum.NO_MORE_DOCS) { + if (liveDocs[seg] == null || liveDocs[seg].get(docID)) { + return docBases[seg] + docID; + } } assert hasDeletions; } diff --git a/lucene/test-framework/src/test/org/apache/lucene/tests/search/TestPerThreadPKLookup.java b/lucene/test-framework/src/test/org/apache/lucene/tests/search/TestPerThreadPKLookup.java new file mode 100644 index 00000000000..2136727838c --- /dev/null +++ b/lucene/test-framework/src/test/org/apache/lucene/tests/search/TestPerThreadPKLookup.java @@ -0,0 +1,70 @@ +/* + * 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.tests.search; + +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.KeywordField; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.index.NoMergePolicy; +import org.apache.lucene.index.Term; +import org.apache.lucene.store.Directory; +import org.apache.lucene.tests.analysis.MockAnalyzer; +import org.apache.lucene.tests.index.PerThreadPKLookup; +import org.apache.lucene.tests.util.LuceneTestCase; + +public class TestPerThreadPKLookup extends LuceneTestCase { + + 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)); + // PK updates will be merged to one update. + writer.updateDocument(new Term("PK", "1"), doc); + writer.flush(); + writer.close(); + + DirectoryReader reader = DirectoryReader.open(dir); + PerThreadPKLookup pk = new PerThreadPKLookup(reader, "PK"); + + int docID = pk.lookup(newBytesRef("1")); + assertEquals(2, docID); + + reader.close(); + dir.close(); + } +} From da41215a678f7c1a72cef558594031a99db90d88 Mon Sep 17 00:00:00 2001 From: Chris Hegarty <62058229+ChrisHegarty@users.noreply.github.com> Date: Wed, 10 Jul 2024 09:39:35 +0100 Subject: [PATCH 39/42] Use a confined Arena for IOContext.READONCE (#13535) Use a confined Arena for IOContext.READONCE. This change will require inputs opened with READONCE to be consumed and closed on the creating thread. Further testing and assertions can be added as a follow up. --- .../simpletext/SimpleTextDocValuesReader.java | 2 +- .../simpletext/SimpleTextPointsReader.java | 2 +- .../org/apache/lucene/store/IOContext.java | 7 ++- .../lucene/store/MemorySegmentIndexInput.java | 43 +++++++++++---- .../MemorySegmentIndexInputProvider.java | 6 +- .../lucene/store/TestMMapDirectory.java | 55 +++++++++++++++++++ .../tests/store/MockDirectoryWrapper.java | 11 ++-- .../tests/store/MockIndexInputWrapper.java | 39 ++++++++++++- .../SlowClosingMockIndexInputWrapper.java | 4 +- .../SlowOpeningMockIndexInputWrapper.java | 5 +- .../lucene/tests/util/LuceneTestCase.java | 12 ++-- 11 files changed, 153 insertions(+), 33 deletions(-) diff --git a/lucene/codecs/src/java/org/apache/lucene/codecs/simpletext/SimpleTextDocValuesReader.java b/lucene/codecs/src/java/org/apache/lucene/codecs/simpletext/SimpleTextDocValuesReader.java index f58ff0873ca..435c2f73fdf 100644 --- a/lucene/codecs/src/java/org/apache/lucene/codecs/simpletext/SimpleTextDocValuesReader.java +++ b/lucene/codecs/src/java/org/apache/lucene/codecs/simpletext/SimpleTextDocValuesReader.java @@ -829,7 +829,7 @@ class SimpleTextDocValuesReader extends DocValuesProducer { clone.seek(0); // checksum is fixed-width encoded with 20 bytes, plus 1 byte for newline (the space is included // in SimpleTextUtil.CHECKSUM): - long footerStartPos = data.length() - (SimpleTextUtil.CHECKSUM.length + 21); + long footerStartPos = clone.length() - (SimpleTextUtil.CHECKSUM.length + 21); ChecksumIndexInput input = new BufferedChecksumIndexInput(clone); while (true) { SimpleTextUtil.readLine(input, scratch); diff --git a/lucene/codecs/src/java/org/apache/lucene/codecs/simpletext/SimpleTextPointsReader.java b/lucene/codecs/src/java/org/apache/lucene/codecs/simpletext/SimpleTextPointsReader.java index be0e98f906a..5d6c41663ca 100644 --- a/lucene/codecs/src/java/org/apache/lucene/codecs/simpletext/SimpleTextPointsReader.java +++ b/lucene/codecs/src/java/org/apache/lucene/codecs/simpletext/SimpleTextPointsReader.java @@ -227,7 +227,7 @@ class SimpleTextPointsReader extends PointsReader { // checksum is fixed-width encoded with 20 bytes, plus 1 byte for newline (the space is included // in SimpleTextUtil.CHECKSUM): - long footerStartPos = dataIn.length() - (SimpleTextUtil.CHECKSUM.length + 21); + long footerStartPos = clone.length() - (SimpleTextUtil.CHECKSUM.length + 21); ChecksumIndexInput input = new BufferedChecksumIndexInput(clone); while (true) { SimpleTextUtil.readLine(input, scratch); diff --git a/lucene/core/src/java/org/apache/lucene/store/IOContext.java b/lucene/core/src/java/org/apache/lucene/store/IOContext.java index b2d82af20f8..f318b3a9015 100644 --- a/lucene/core/src/java/org/apache/lucene/store/IOContext.java +++ b/lucene/core/src/java/org/apache/lucene/store/IOContext.java @@ -55,7 +55,12 @@ public record IOContext( */ public static final IOContext DEFAULT = new IOContext(Constants.DEFAULT_READADVICE); - /** A default context for reads with {@link ReadAdvice#SEQUENTIAL}. */ + /** + * A default context for reads with {@link ReadAdvice#SEQUENTIAL}. + * + *

    This context should only be used when the read operations will be performed in the same + * thread as the thread that opens the underlying storage. + */ public static final IOContext READONCE = new IOContext(ReadAdvice.SEQUENTIAL); @SuppressWarnings("incomplete-switch") diff --git a/lucene/core/src/java21/org/apache/lucene/store/MemorySegmentIndexInput.java b/lucene/core/src/java21/org/apache/lucene/store/MemorySegmentIndexInput.java index 68f1e771195..e9805f0f7a6 100644 --- a/lucene/core/src/java21/org/apache/lucene/store/MemorySegmentIndexInput.java +++ b/lucene/core/src/java21/org/apache/lucene/store/MemorySegmentIndexInput.java @@ -53,6 +53,7 @@ abstract class MemorySegmentIndexInput extends IndexInput final long length; final long chunkSizeMask; final int chunkSizePower; + final boolean confined; final Arena arena; final MemorySegment[] segments; @@ -67,12 +68,15 @@ abstract class MemorySegmentIndexInput extends IndexInput Arena arena, MemorySegment[] segments, long length, - int chunkSizePower) { + int chunkSizePower, + boolean confined) { assert Arrays.stream(segments).map(MemorySegment::scope).allMatch(arena.scope()::equals); if (segments.length == 1) { - return new SingleSegmentImpl(resourceDescription, arena, segments[0], length, chunkSizePower); + return new SingleSegmentImpl( + resourceDescription, arena, segments[0], length, chunkSizePower, confined); } else { - return new MultiSegmentImpl(resourceDescription, arena, segments, 0, length, chunkSizePower); + return new MultiSegmentImpl( + resourceDescription, arena, segments, 0, length, chunkSizePower, confined); } } @@ -81,12 +85,14 @@ abstract class MemorySegmentIndexInput extends IndexInput Arena arena, MemorySegment[] segments, long length, - int chunkSizePower) { + int chunkSizePower, + boolean confined) { super(resourceDescription); this.arena = arena; this.segments = segments; this.length = length; this.chunkSizePower = chunkSizePower; + this.confined = confined; this.chunkSizeMask = (1L << chunkSizePower) - 1L; this.curSegment = segments[0]; } @@ -97,6 +103,12 @@ abstract class MemorySegmentIndexInput extends IndexInput } } + void ensureAccessible() { + if (confined && curSegment.isAccessibleBy(Thread.currentThread()) == false) { + throw new IllegalStateException("confined"); + } + } + // the unused parameter is just to silence javac about unused variables RuntimeException handlePositionalIOOBE(RuntimeException unused, String action, long pos) throws IOException { @@ -570,6 +582,7 @@ abstract class MemorySegmentIndexInput extends IndexInput /** Builds the actual sliced IndexInput (may apply extra offset in subclasses). * */ MemorySegmentIndexInput buildSlice(String sliceDescription, long offset, long length) { ensureOpen(); + ensureAccessible(); final long sliceEnd = offset + length; final int startIndex = (int) (offset >>> chunkSizePower); @@ -591,7 +604,8 @@ abstract class MemorySegmentIndexInput extends IndexInput null, // clones don't have an Arena, as they can't close) slices[0].asSlice(offset, length), length, - chunkSizePower); + chunkSizePower, + confined); } else { return new MultiSegmentImpl( newResourceDescription, @@ -599,7 +613,8 @@ abstract class MemorySegmentIndexInput extends IndexInput slices, offset, length, - chunkSizePower); + chunkSizePower, + confined); } } @@ -643,8 +658,15 @@ abstract class MemorySegmentIndexInput extends IndexInput Arena arena, MemorySegment segment, long length, - int chunkSizePower) { - super(resourceDescription, arena, new MemorySegment[] {segment}, length, chunkSizePower); + int chunkSizePower, + boolean confined) { + super( + resourceDescription, + arena, + new MemorySegment[] {segment}, + length, + chunkSizePower, + confined); this.curSegmentIndex = 0; } @@ -740,8 +762,9 @@ abstract class MemorySegmentIndexInput extends IndexInput MemorySegment[] segments, long offset, long length, - int chunkSizePower) { - super(resourceDescription, arena, segments, length, chunkSizePower); + int chunkSizePower, + boolean confined) { + super(resourceDescription, arena, segments, length, chunkSizePower, confined); this.offset = offset; try { seek(0L); diff --git a/lucene/core/src/java21/org/apache/lucene/store/MemorySegmentIndexInputProvider.java b/lucene/core/src/java21/org/apache/lucene/store/MemorySegmentIndexInputProvider.java index e1655101d75..08f6149746b 100644 --- a/lucene/core/src/java21/org/apache/lucene/store/MemorySegmentIndexInputProvider.java +++ b/lucene/core/src/java21/org/apache/lucene/store/MemorySegmentIndexInputProvider.java @@ -45,7 +45,8 @@ final class MemorySegmentIndexInputProvider implements MMapDirectory.MMapIndexIn path = Unwrappable.unwrapAll(path); boolean success = false; - final Arena arena = Arena.ofShared(); + final boolean confined = context == IOContext.READONCE; + final Arena arena = confined ? Arena.ofConfined() : Arena.ofShared(); try (var fc = FileChannel.open(path, StandardOpenOption.READ)) { final long fileSize = fc.size(); final IndexInput in = @@ -61,7 +62,8 @@ final class MemorySegmentIndexInputProvider implements MMapDirectory.MMapIndexIn preload, fileSize), fileSize, - chunkSizePower); + chunkSizePower, + confined); success = true; return in; } finally { diff --git a/lucene/core/src/test/org/apache/lucene/store/TestMMapDirectory.java b/lucene/core/src/test/org/apache/lucene/store/TestMMapDirectory.java index 39d3dbda9ac..f7c49c9b661 100644 --- a/lucene/core/src/test/org/apache/lucene/store/TestMMapDirectory.java +++ b/lucene/core/src/test/org/apache/lucene/store/TestMMapDirectory.java @@ -19,9 +19,14 @@ package org.apache.lucene.store; import java.io.IOException; import java.nio.file.Path; import java.util.Random; +import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import org.apache.lucene.tests.store.BaseDirectoryTestCase; import org.apache.lucene.util.Constants; +import org.apache.lucene.util.NamedThreadFactory; /** Tests MMapDirectory */ // See: https://issues.apache.org/jira/browse/SOLR-12028 Tests cannot remove files on Windows @@ -117,4 +122,54 @@ public class TestMMapDirectory extends BaseDirectoryTestCase { } } } + + // Opens the input with ReadAdvice.READONCE to ensure slice and clone are appropriately confined + public void testConfined() throws Exception { + final int size = 16; + byte[] bytes = new byte[size]; + random().nextBytes(bytes); + + try (Directory dir = new MMapDirectory(createTempDir("testConfined"))) { + try (IndexOutput out = dir.createOutput("test", IOContext.DEFAULT)) { + out.writeBytes(bytes, 0, bytes.length); + } + + try (var in = dir.openInput("test", IOContext.READONCE); + var executor = Executors.newFixedThreadPool(1, new NamedThreadFactory("testConfined"))) { + // ensure accessible + assertEquals(16L, in.slice("test", 0, in.length()).length()); + assertEquals(15L, in.slice("test", 1, in.length() - 1).length()); + + // ensure not accessible + Callable task1 = () -> in.slice("test", 0, in.length()); + var x = expectThrows(ISE, () -> getAndUnwrap(executor.submit(task1))); + assertTrue(x.getMessage().contains("confined")); + + int offset = random().nextInt((int) in.length()); + int length = (int) in.length() - offset; + Callable task2 = () -> in.slice("test", offset, length); + x = expectThrows(ISE, () -> getAndUnwrap(executor.submit(task2))); + assertTrue(x.getMessage().contains("confined")); + + // slice.slice + var slice = in.slice("test", 0, in.length()); + Callable task3 = () -> slice.slice("test", 0, in.length()); + x = expectThrows(ISE, () -> getAndUnwrap(executor.submit(task3))); + assertTrue(x.getMessage().contains("confined")); + // slice.clone + x = expectThrows(ISE, () -> getAndUnwrap(executor.submit(slice::clone))); + assertTrue(x.getMessage().contains("confined")); + } + } + } + + static final Class ISE = IllegalStateException.class; + + static Object getAndUnwrap(Future future) throws Throwable { + try { + return future.get(); + } catch (ExecutionException ee) { + throw ee.getCause(); + } + } } diff --git a/lucene/test-framework/src/java/org/apache/lucene/tests/store/MockDirectoryWrapper.java b/lucene/test-framework/src/java/org/apache/lucene/tests/store/MockDirectoryWrapper.java index 0411a2c183b..2589d082fc9 100644 --- a/lucene/test-framework/src/java/org/apache/lucene/tests/store/MockDirectoryWrapper.java +++ b/lucene/test-framework/src/java/org/apache/lucene/tests/store/MockDirectoryWrapper.java @@ -812,8 +812,9 @@ public class MockDirectoryWrapper extends BaseDirectoryWrapper { false); } - IndexInput delegateInput = - in.openInput(name, LuceneTestCase.newIOContext(randomState, context)); + context = LuceneTestCase.newIOContext(randomState, context); + final boolean confined = context == IOContext.READONCE; + IndexInput delegateInput = in.openInput(name, context); final IndexInput ii; int randomInt = randomState.nextInt(500); @@ -822,15 +823,15 @@ public class MockDirectoryWrapper extends BaseDirectoryWrapper { System.out.println( "MockDirectoryWrapper: using SlowClosingMockIndexInputWrapper for file " + name); } - ii = new SlowClosingMockIndexInputWrapper(this, name, delegateInput); + ii = new SlowClosingMockIndexInputWrapper(this, name, delegateInput, confined); } else if (useSlowOpenClosers && randomInt == 1) { if (LuceneTestCase.VERBOSE) { System.out.println( "MockDirectoryWrapper: using SlowOpeningMockIndexInputWrapper for file " + name); } - ii = new SlowOpeningMockIndexInputWrapper(this, name, delegateInput); + ii = new SlowOpeningMockIndexInputWrapper(this, name, delegateInput, confined); } else { - ii = new MockIndexInputWrapper(this, name, delegateInput, null); + ii = new MockIndexInputWrapper(this, name, delegateInput, null, confined); } addFileHandle(ii, name, Handle.Input); return ii; diff --git a/lucene/test-framework/src/java/org/apache/lucene/tests/store/MockIndexInputWrapper.java b/lucene/test-framework/src/java/org/apache/lucene/tests/store/MockIndexInputWrapper.java index b25bd155783..87279008614 100644 --- a/lucene/test-framework/src/java/org/apache/lucene/tests/store/MockIndexInputWrapper.java +++ b/lucene/test-framework/src/java/org/apache/lucene/tests/store/MockIndexInputWrapper.java @@ -39,10 +39,16 @@ public class MockIndexInputWrapper extends FilterIndexInput { // Which MockIndexInputWrapper we were cloned from, or null if we are not a clone: private final MockIndexInputWrapper parent; + private final boolean confined; + private final Thread thread; /** Sole constructor */ public MockIndexInputWrapper( - MockDirectoryWrapper dir, String name, IndexInput delegate, MockIndexInputWrapper parent) { + MockDirectoryWrapper dir, + String name, + IndexInput delegate, + MockIndexInputWrapper parent, + boolean confined) { super("MockIndexInputWrapper(name=" + name + " delegate=" + delegate + ")", delegate); // If we are a clone then our parent better not be a clone! @@ -51,6 +57,8 @@ public class MockIndexInputWrapper extends FilterIndexInput { this.parent = parent; this.name = name; this.dir = dir; + this.confined = confined; + this.thread = Thread.currentThread(); } @Override @@ -84,6 +92,12 @@ public class MockIndexInputWrapper extends FilterIndexInput { } } + private void ensureAccessible() { + if (confined && thread != Thread.currentThread()) { + throw new RuntimeException("Abusing from another thread!"); + } + } + @Override public MockIndexInputWrapper clone() { ensureOpen(); @@ -93,7 +107,7 @@ public class MockIndexInputWrapper extends FilterIndexInput { dir.inputCloneCount.incrementAndGet(); IndexInput iiclone = in.clone(); MockIndexInputWrapper clone = - new MockIndexInputWrapper(dir, name, iiclone, parent != null ? parent : this); + new MockIndexInputWrapper(dir, name, iiclone, parent != null ? parent : this, confined); // Pending resolution on LUCENE-686 we may want to // uncomment this code so that we also track that all // clones get closed: @@ -120,25 +134,29 @@ public class MockIndexInputWrapper extends FilterIndexInput { dir.inputCloneCount.incrementAndGet(); IndexInput slice = in.slice(sliceDescription, offset, length); MockIndexInputWrapper clone = - new MockIndexInputWrapper(dir, sliceDescription, slice, parent != null ? parent : this); + new MockIndexInputWrapper( + dir, sliceDescription, slice, parent != null ? parent : this, confined); return clone; } @Override public long getFilePointer() { ensureOpen(); + ensureAccessible(); return in.getFilePointer(); } @Override public void seek(long pos) throws IOException { ensureOpen(); + ensureAccessible(); in.seek(pos); } @Override public void prefetch(long offset, long length) throws IOException { ensureOpen(); + ensureAccessible(); in.prefetch(offset, length); } @@ -151,90 +169,105 @@ public class MockIndexInputWrapper extends FilterIndexInput { @Override public byte readByte() throws IOException { ensureOpen(); + ensureAccessible(); return in.readByte(); } @Override public void readBytes(byte[] b, int offset, int len) throws IOException { ensureOpen(); + ensureAccessible(); in.readBytes(b, offset, len); } @Override public void readBytes(byte[] b, int offset, int len, boolean useBuffer) throws IOException { ensureOpen(); + ensureAccessible(); in.readBytes(b, offset, len, useBuffer); } @Override public void readFloats(float[] floats, int offset, int len) throws IOException { ensureOpen(); + ensureAccessible(); in.readFloats(floats, offset, len); } @Override public short readShort() throws IOException { ensureOpen(); + ensureAccessible(); return in.readShort(); } @Override public int readInt() throws IOException { ensureOpen(); + ensureAccessible(); return in.readInt(); } @Override public long readLong() throws IOException { ensureOpen(); + ensureAccessible(); return in.readLong(); } @Override public String readString() throws IOException { ensureOpen(); + ensureAccessible(); return in.readString(); } @Override public int readVInt() throws IOException { ensureOpen(); + ensureAccessible(); return in.readVInt(); } @Override public long readVLong() throws IOException { ensureOpen(); + ensureAccessible(); return in.readVLong(); } @Override public int readZInt() throws IOException { ensureOpen(); + ensureAccessible(); return in.readZInt(); } @Override public long readZLong() throws IOException { ensureOpen(); + ensureAccessible(); return in.readZLong(); } @Override public void skipBytes(long numBytes) throws IOException { ensureOpen(); + ensureAccessible(); super.skipBytes(numBytes); } @Override public Map readMapOfStrings() throws IOException { ensureOpen(); + ensureAccessible(); return in.readMapOfStrings(); } @Override public Set readSetOfStrings() throws IOException { ensureOpen(); + ensureAccessible(); return in.readSetOfStrings(); } diff --git a/lucene/test-framework/src/java/org/apache/lucene/tests/store/SlowClosingMockIndexInputWrapper.java b/lucene/test-framework/src/java/org/apache/lucene/tests/store/SlowClosingMockIndexInputWrapper.java index 73197a66155..1f9e61f5195 100644 --- a/lucene/test-framework/src/java/org/apache/lucene/tests/store/SlowClosingMockIndexInputWrapper.java +++ b/lucene/test-framework/src/java/org/apache/lucene/tests/store/SlowClosingMockIndexInputWrapper.java @@ -35,8 +35,8 @@ class SlowClosingMockIndexInputWrapper extends MockIndexInputWrapper { } public SlowClosingMockIndexInputWrapper( - MockDirectoryWrapper dir, String name, IndexInput delegate) { - super(dir, name, delegate, null); + MockDirectoryWrapper dir, String name, IndexInput delegate, boolean confined) { + super(dir, name, delegate, null, confined); } @SuppressForbidden(reason = "Thread sleep") diff --git a/lucene/test-framework/src/java/org/apache/lucene/tests/store/SlowOpeningMockIndexInputWrapper.java b/lucene/test-framework/src/java/org/apache/lucene/tests/store/SlowOpeningMockIndexInputWrapper.java index da0e13537c9..033785af9c7 100644 --- a/lucene/test-framework/src/java/org/apache/lucene/tests/store/SlowOpeningMockIndexInputWrapper.java +++ b/lucene/test-framework/src/java/org/apache/lucene/tests/store/SlowOpeningMockIndexInputWrapper.java @@ -35,8 +35,9 @@ class SlowOpeningMockIndexInputWrapper extends MockIndexInputWrapper { @SuppressForbidden(reason = "Thread sleep") public SlowOpeningMockIndexInputWrapper( - MockDirectoryWrapper dir, String name, IndexInput delegate) throws IOException { - super(dir, name, delegate, null); + MockDirectoryWrapper dir, String name, IndexInput delegate, boolean confined) + throws IOException { + super(dir, name, delegate, null, confined); try { Thread.sleep(50); } catch (InterruptedException ie) { diff --git a/lucene/test-framework/src/java/org/apache/lucene/tests/util/LuceneTestCase.java b/lucene/test-framework/src/java/org/apache/lucene/tests/util/LuceneTestCase.java index c649fd18fa5..c61968d557e 100644 --- a/lucene/test-framework/src/java/org/apache/lucene/tests/util/LuceneTestCase.java +++ b/lucene/test-framework/src/java/org/apache/lucene/tests/util/LuceneTestCase.java @@ -1780,6 +1780,9 @@ public abstract class LuceneTestCase extends Assert { /** TODO: javadoc */ public static IOContext newIOContext(Random random, IOContext oldContext) { + if (oldContext == IOContext.READONCE) { + return oldContext; // don't mess with the READONCE singleton + } final int randomNumDocs = random.nextInt(4192); final int size = random.nextInt(512) * randomNumDocs; if (oldContext.flushInfo() != null) { @@ -1798,19 +1801,16 @@ public abstract class LuceneTestCase extends Assert { random.nextBoolean(), TestUtil.nextInt(random, 1, 100))); } else { - // Make a totally random IOContext: + // Make a totally random IOContext, except READONCE which has semantic implications final IOContext context; - switch (random.nextInt(4)) { + switch (random.nextInt(3)) { case 0: context = IOContext.DEFAULT; break; case 1: - context = IOContext.READONCE; - break; - case 2: context = new IOContext(new MergeInfo(randomNumDocs, size, true, -1)); break; - case 3: + case 2: context = new IOContext(new FlushInfo(randomNumDocs, size)); break; default: From 026d661e5fd91f879de8a429687c70b6eb9b9f9d Mon Sep 17 00:00:00 2001 From: Adrien Grand Date: Wed, 10 Jul 2024 15:36:35 +0200 Subject: [PATCH 40/42] Use `IndexInput#prefetch` for terms dictionary lookups. (#13359) This introduces `TermsEnum#prepareSeekExact`, which essentially calls `IndexInput#prefetch` at the right offset for the given term. Then it takes advantage of the fact that `BooleanQuery` already calls `Weight#scorerSupplier` on all clauses, before later calling `ScorerSupplier#get` on all clauses. So `TermQuery` now calls `TermsEnum#prepareSeekExact` on `Weight#scorerSupplier` (if scores are not needed), which in-turn means that the I/O all terms dictionary lookups get parallelized across all term queries of a `BooleanQuery` on a given segment (intra-segment parallelism). --- .../bloom/BloomFilteringPostingsFormat.java | 12 +- .../sharedterms/STMergingTermsEnum.java | 9 +- .../lucene/codecs/DocValuesConsumer.java | 13 +- .../lucene90/blocktree/SegmentTermsEnum.java | 94 ++++++---- .../blocktree/SegmentTermsEnumFrame.java | 15 ++ .../apache/lucene/index/BaseTermsEnum.java | 6 + .../org/apache/lucene/index/CheckIndex.java | 11 +- .../apache/lucene/index/FilterLeafReader.java | 7 + .../lucene/index/FilteredTermsEnum.java | 11 ++ .../org/apache/lucene/index/TermStates.java | 147 ++++++++-------- .../org/apache/lucene/index/TermsEnum.java | 36 ++-- .../lucene/search/BlendedTermQuery.java | 7 +- .../apache/lucene/search/FuzzyTermsEnum.java | 9 +- .../lucene/search/MultiPhraseQuery.java | 4 +- .../org/apache/lucene/search/PhraseQuery.java | 4 +- .../apache/lucene/search/SynonymQuery.java | 164 +++++++++++------- .../org/apache/lucene/search/TermQuery.java | 39 ++++- .../apache/lucene/util/IOBooleanSupplier.java | 37 ++++ .../lucene/search/TestBooleanRewrites.java | 2 +- .../lucene/search/TestBooleanScorer.java | 2 +- .../apache/lucene/search/TestTermQuery.java | 6 + .../lucene/search/join/TestBlockJoin.java | 2 +- .../lucene/queries/spans/SpanTermQuery.java | 4 +- .../sandbox/search/CombinedFieldQuery.java | 4 +- .../sandbox/search/PhraseWildcardQuery.java | 4 +- .../sandbox/search/TermAutomatonQuery.java | 4 +- .../tests/index/AssertingLeafReader.java | 29 +++- 27 files changed, 452 insertions(+), 230 deletions(-) create mode 100644 lucene/core/src/java/org/apache/lucene/util/IOBooleanSupplier.java diff --git a/lucene/codecs/src/java/org/apache/lucene/codecs/bloom/BloomFilteringPostingsFormat.java b/lucene/codecs/src/java/org/apache/lucene/codecs/bloom/BloomFilteringPostingsFormat.java index 2c908fcabe3..1daa1761fd8 100644 --- a/lucene/codecs/src/java/org/apache/lucene/codecs/bloom/BloomFilteringPostingsFormat.java +++ b/lucene/codecs/src/java/org/apache/lucene/codecs/bloom/BloomFilteringPostingsFormat.java @@ -43,6 +43,7 @@ import org.apache.lucene.store.ChecksumIndexInput; import org.apache.lucene.store.DataOutput; import org.apache.lucene.store.IndexOutput; import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.IOBooleanSupplier; import org.apache.lucene.util.IOUtils; import org.apache.lucene.util.automaton.CompiledAutomaton; @@ -315,12 +316,21 @@ public final class BloomFilteringPostingsFormat extends PostingsFormat { } @Override - public boolean seekExact(BytesRef text) throws IOException { + public IOBooleanSupplier prepareSeekExact(BytesRef text) throws IOException { // The magical fail-fast speed up that is the entire point of all of // this code - save a disk seek if there is a match on an in-memory // structure // that may occasionally give a false positive but guaranteed no false // negatives + if (filter.contains(text) == ContainsResult.NO) { + return null; + } + return delegate().prepareSeekExact(text); + } + + @Override + public boolean seekExact(BytesRef text) throws IOException { + // See #prepareSeekExact if (filter.contains(text) == ContainsResult.NO) { return false; } diff --git a/lucene/codecs/src/java/org/apache/lucene/codecs/uniformsplit/sharedterms/STMergingTermsEnum.java b/lucene/codecs/src/java/org/apache/lucene/codecs/uniformsplit/sharedterms/STMergingTermsEnum.java index 7a772c38908..95d63b14e3d 100644 --- a/lucene/codecs/src/java/org/apache/lucene/codecs/uniformsplit/sharedterms/STMergingTermsEnum.java +++ b/lucene/codecs/src/java/org/apache/lucene/codecs/uniformsplit/sharedterms/STMergingTermsEnum.java @@ -20,11 +20,11 @@ package org.apache.lucene.codecs.uniformsplit.sharedterms; import java.io.IOException; import java.util.List; import java.util.RandomAccess; +import org.apache.lucene.index.BaseTermsEnum; import org.apache.lucene.index.ImpactsEnum; import org.apache.lucene.index.MergeState; import org.apache.lucene.index.PostingsEnum; import org.apache.lucene.index.TermState; -import org.apache.lucene.index.TermsEnum; import org.apache.lucene.util.AttributeSource; import org.apache.lucene.util.BytesRef; @@ -34,7 +34,7 @@ import org.apache.lucene.util.BytesRef; * * @lucene.experimental */ -class STMergingTermsEnum extends TermsEnum { +class STMergingTermsEnum extends BaseTermsEnum { protected final String fieldName; protected final MultiSegmentsPostingsEnum multiPostingsEnum; @@ -63,11 +63,6 @@ class STMergingTermsEnum extends TermsEnum { throw new UnsupportedOperationException(); } - @Override - public boolean seekExact(BytesRef text) throws IOException { - throw new UnsupportedOperationException(); - } - @Override public SeekStatus seekCeil(BytesRef text) { throw new UnsupportedOperationException(); diff --git a/lucene/core/src/java/org/apache/lucene/codecs/DocValuesConsumer.java b/lucene/core/src/java/org/apache/lucene/codecs/DocValuesConsumer.java index 0d171812fae..cbb906788e5 100644 --- a/lucene/core/src/java/org/apache/lucene/codecs/DocValuesConsumer.java +++ b/lucene/core/src/java/org/apache/lucene/codecs/DocValuesConsumer.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import org.apache.lucene.index.BaseTermsEnum; import org.apache.lucene.index.BinaryDocValues; import org.apache.lucene.index.DocIDMerger; import org.apache.lucene.index.DocValues; @@ -498,7 +499,7 @@ public abstract class DocValuesConsumer implements Closeable { * {@link SortedDocValues#lookupOrd(int)} or {@link SortedSetDocValues#lookupOrd(long)} on every * call to {@link TermsEnum#next()}. */ - private static class MergedTermsEnum extends TermsEnum { + private static class MergedTermsEnum extends BaseTermsEnum { private final TermsEnum[] subs; private final OrdinalMap ordinalMap; @@ -542,11 +543,6 @@ public abstract class DocValuesConsumer implements Closeable { throw new UnsupportedOperationException(); } - @Override - public boolean seekExact(BytesRef text) throws IOException { - throw new UnsupportedOperationException(); - } - @Override public SeekStatus seekCeil(BytesRef text) throws IOException { throw new UnsupportedOperationException(); @@ -557,11 +553,6 @@ public abstract class DocValuesConsumer implements Closeable { throw new UnsupportedOperationException(); } - @Override - public void seekExact(BytesRef term, TermState state) throws IOException { - throw new UnsupportedOperationException(); - } - @Override public int docFreq() throws IOException { throw new UnsupportedOperationException(); diff --git a/lucene/core/src/java/org/apache/lucene/codecs/lucene90/blocktree/SegmentTermsEnum.java b/lucene/core/src/java/org/apache/lucene/codecs/lucene90/blocktree/SegmentTermsEnum.java index e3389931be7..91776585407 100644 --- a/lucene/core/src/java/org/apache/lucene/codecs/lucene90/blocktree/SegmentTermsEnum.java +++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene90/blocktree/SegmentTermsEnum.java @@ -30,6 +30,7 @@ import org.apache.lucene.store.IndexInput; import org.apache.lucene.util.ArrayUtil; import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.BytesRefBuilder; +import org.apache.lucene.util.IOBooleanSupplier; import org.apache.lucene.util.RamUsageEstimator; import org.apache.lucene.util.fst.FST; import org.apache.lucene.util.fst.Util; @@ -307,15 +308,13 @@ final class SegmentTermsEnum extends BaseTermsEnum { return true; } - @Override - public boolean seekExact(BytesRef target) throws IOException { - + private IOBooleanSupplier prepareSeekExact(BytesRef target, boolean prefetch) throws IOException { if (fr.index == null) { throw new IllegalStateException("terms index was not loaded"); } if (fr.size() > 0 && (target.compareTo(fr.getMin()) < 0 || target.compareTo(fr.getMax()) > 0)) { - return false; + return null; } term.grow(1 + target.length); @@ -431,7 +430,7 @@ final class SegmentTermsEnum extends BaseTermsEnum { // if (DEBUG) { // System.out.println(" target is same as current; return true"); // } - return true; + return () -> true; } else { // if (DEBUG) { // System.out.println(" target is same as current but term doesn't exist"); @@ -501,24 +500,30 @@ final class SegmentTermsEnum extends BaseTermsEnum { // if (DEBUG) { // System.out.println(" FAST NOT_FOUND term=" + ToStringUtils.bytesRefToString(term)); // } - return false; + return null; } - currentFrame.loadBlock(); - - final SeekStatus result = currentFrame.scanToTerm(target, true); - if (result == SeekStatus.FOUND) { - // if (DEBUG) { - // System.out.println(" return FOUND term=" + term.utf8ToString() + " " + term); - // } - return true; - } else { - // if (DEBUG) { - // System.out.println(" got " + result + "; return NOT_FOUND term=" + - // ToStringUtils.bytesRefToString(term)); - // } - return false; + if (prefetch) { + currentFrame.prefetchBlock(); } + + return () -> { + currentFrame.loadBlock(); + + final SeekStatus result = currentFrame.scanToTerm(target, true); + if (result == SeekStatus.FOUND) { + // if (DEBUG) { + // System.out.println(" return FOUND term=" + term.utf8ToString() + " " + term); + // } + return true; + } else { + // if (DEBUG) { + // System.out.println(" got " + result + "; return NOT_FOUND term=" + + // ToStringUtils.bytesRefToString(term)); + // } + return false; + } + }; } else { // Follow this arc arc = nextArc; @@ -556,25 +561,42 @@ final class SegmentTermsEnum extends BaseTermsEnum { // if (DEBUG) { // System.out.println(" FAST NOT_FOUND term=" + ToStringUtils.bytesRefToString(term)); // } - return false; + return null; } - currentFrame.loadBlock(); - - final SeekStatus result = currentFrame.scanToTerm(target, true); - if (result == SeekStatus.FOUND) { - // if (DEBUG) { - // System.out.println(" return FOUND term=" + term.utf8ToString() + " " + term); - // } - return true; - } else { - // if (DEBUG) { - // System.out.println(" got result " + result + "; return NOT_FOUND term=" + - // term.utf8ToString()); - // } - - return false; + if (prefetch) { + currentFrame.prefetchBlock(); } + + return () -> { + currentFrame.loadBlock(); + + final SeekStatus result = currentFrame.scanToTerm(target, true); + if (result == SeekStatus.FOUND) { + // if (DEBUG) { + // System.out.println(" return FOUND term=" + term.utf8ToString() + " " + term); + // } + return true; + } else { + // if (DEBUG) { + // System.out.println(" got result " + result + "; return NOT_FOUND term=" + + // term.utf8ToString()); + // } + + return false; + } + }; + } + + @Override + public IOBooleanSupplier prepareSeekExact(BytesRef target) throws IOException { + return prepareSeekExact(target, true); + } + + @Override + public boolean seekExact(BytesRef target) throws IOException { + IOBooleanSupplier termExistsSupplier = prepareSeekExact(target, false); + return termExistsSupplier != null && termExistsSupplier.get(); } @Override diff --git a/lucene/core/src/java/org/apache/lucene/codecs/lucene90/blocktree/SegmentTermsEnumFrame.java b/lucene/core/src/java/org/apache/lucene/codecs/lucene90/blocktree/SegmentTermsEnumFrame.java index c1552c8ce60..5ecbc3c173e 100644 --- a/lucene/core/src/java/org/apache/lucene/codecs/lucene90/blocktree/SegmentTermsEnumFrame.java +++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene90/blocktree/SegmentTermsEnumFrame.java @@ -133,6 +133,21 @@ final class SegmentTermsEnumFrame { loadBlock(); } + void prefetchBlock() throws IOException { + if (nextEnt != -1) { + // Already loaded + return; + } + + // Clone the IndexInput lazily, so that consumers + // that just pull a TermsEnum to + // seekExact(TermState) don't pay this cost: + ste.initIndexInput(); + + // TODO: Could we know the number of bytes to prefetch? + ste.in.prefetch(fp, 1); + } + /* Does initial decode of next block of terms; this doesn't actually decode the docFreq, totalTermFreq, postings details (frq/prx offset, etc.) metadata; diff --git a/lucene/core/src/java/org/apache/lucene/index/BaseTermsEnum.java b/lucene/core/src/java/org/apache/lucene/index/BaseTermsEnum.java index 37b8395aebb..6809a7a8682 100644 --- a/lucene/core/src/java/org/apache/lucene/index/BaseTermsEnum.java +++ b/lucene/core/src/java/org/apache/lucene/index/BaseTermsEnum.java @@ -20,6 +20,7 @@ package org.apache.lucene.index; import java.io.IOException; import org.apache.lucene.util.AttributeSource; import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.IOBooleanSupplier; /** * A base TermsEnum that adds default implementations for @@ -58,6 +59,11 @@ public abstract class BaseTermsEnum extends TermsEnum { return seekCeil(text) == SeekStatus.FOUND; } + @Override + public IOBooleanSupplier prepareSeekExact(BytesRef text) throws IOException { + return () -> seekExact(text); + } + @Override public void seekExact(BytesRef term, TermState state) throws IOException { if (!seekExact(term)) { diff --git a/lucene/core/src/java/org/apache/lucene/index/CheckIndex.java b/lucene/core/src/java/org/apache/lucene/index/CheckIndex.java index 0f6020f7873..aaa76f418a9 100644 --- a/lucene/core/src/java/org/apache/lucene/index/CheckIndex.java +++ b/lucene/core/src/java/org/apache/lucene/index/CheckIndex.java @@ -79,6 +79,7 @@ import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.BytesRefBuilder; import org.apache.lucene.util.CommandLineUtil; import org.apache.lucene.util.FixedBitSet; +import org.apache.lucene.util.IOBooleanSupplier; import org.apache.lucene.util.IOUtils; import org.apache.lucene.util.LongBitSet; import org.apache.lucene.util.NamedThreadFactory; @@ -3869,6 +3870,7 @@ public final class CheckIndex implements Closeable { TermsEnum postingsTermsEnum = postingsTerms.iterator(); final boolean hasProx = terms.hasOffsets() || terms.hasPositions(); + int seekExactCounter = 0; BytesRef term; while ((term = termsEnum.next()) != null) { @@ -3876,7 +3878,14 @@ public final class CheckIndex implements Closeable { postings = termsEnum.postings(postings, PostingsEnum.ALL); assert postings != null; - if (postingsTermsEnum.seekExact(term) == false) { + boolean termExists; + if ((seekExactCounter++ & 0x01) == 0) { + termExists = postingsTermsEnum.seekExact(term); + } else { + IOBooleanSupplier termExistsSupplier = postingsTermsEnum.prepareSeekExact(term); + termExists = termExistsSupplier != null && termExistsSupplier.get(); + } + if (termExists == false) { throw new CheckIndexException( "vector term=" + term diff --git a/lucene/core/src/java/org/apache/lucene/index/FilterLeafReader.java b/lucene/core/src/java/org/apache/lucene/index/FilterLeafReader.java index 4935237178a..87d62f22d04 100644 --- a/lucene/core/src/java/org/apache/lucene/index/FilterLeafReader.java +++ b/lucene/core/src/java/org/apache/lucene/index/FilterLeafReader.java @@ -22,6 +22,7 @@ import org.apache.lucene.search.KnnCollector; import org.apache.lucene.util.AttributeSource; import org.apache.lucene.util.Bits; import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.IOBooleanSupplier; import org.apache.lucene.util.Unwrappable; /** @@ -161,6 +162,7 @@ public abstract class FilterLeafReader extends LeafReader { /** Base class for filtering {@link TermsEnum} implementations. */ public abstract static class FilterTermsEnum extends TermsEnum { + /** The underlying TermsEnum instance. */ protected final TermsEnum in; @@ -236,6 +238,11 @@ public abstract class FilterLeafReader extends LeafReader { in.seekExact(term, state); } + @Override + public IOBooleanSupplier prepareSeekExact(BytesRef text) throws IOException { + return in.prepareSeekExact(text); + } + @Override public TermState termState() throws IOException { return in.termState(); diff --git a/lucene/core/src/java/org/apache/lucene/index/FilteredTermsEnum.java b/lucene/core/src/java/org/apache/lucene/index/FilteredTermsEnum.java index 5ee99878567..c8354cd881f 100644 --- a/lucene/core/src/java/org/apache/lucene/index/FilteredTermsEnum.java +++ b/lucene/core/src/java/org/apache/lucene/index/FilteredTermsEnum.java @@ -19,6 +19,7 @@ package org.apache.lucene.index; import java.io.IOException; import org.apache.lucene.util.AttributeSource; import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.IOBooleanSupplier; /** * Abstract class for enumerating a subset of all terms. @@ -155,6 +156,16 @@ public abstract class FilteredTermsEnum extends TermsEnum { throw new UnsupportedOperationException(getClass().getName() + " does not support seeking"); } + /** + * This enum does not support seeking! + * + * @throws UnsupportedOperationException In general, subclasses do not support seeking. + */ + @Override + public IOBooleanSupplier prepareSeekExact(BytesRef text) throws IOException { + throw new UnsupportedOperationException(getClass().getName() + " does not support seeking"); + } + /** * This enum does not support seeking! * diff --git a/lucene/core/src/java/org/apache/lucene/index/TermStates.java b/lucene/core/src/java/org/apache/lucene/index/TermStates.java index 75c64a907b1..2321104d30f 100644 --- a/lucene/core/src/java/org/apache/lucene/index/TermStates.java +++ b/lucene/core/src/java/org/apache/lucene/index/TermStates.java @@ -17,12 +17,12 @@ package org.apache.lucene.index; import java.io.IOException; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; -import java.util.concurrent.Callable; +import java.util.function.Supplier; import org.apache.lucene.search.IndexSearcher; -import org.apache.lucene.search.TaskExecutor; +import org.apache.lucene.util.ArrayUtil; +import org.apache.lucene.util.IOBooleanSupplier; +import org.apache.lucene.util.IOSupplier; /** * Maintains a {@link IndexReader} {@link TermState} view over {@link IndexReader} instances @@ -80,6 +80,8 @@ public final class TermStates { register(state, ord, docFreq, totalTermFreq); } + private record PendingTermLookup(TermsEnum termsEnum, IOBooleanSupplier supplier) {} + /** * Creates a {@link TermStates} from a top-level {@link IndexReaderContext} and the given {@link * Term}. This method will lookup the given term in all context's leaf readers and register each @@ -97,42 +99,29 @@ public final class TermStates { assert context != null; final TermStates perReaderTermState = new TermStates(needsStats ? null : term, context); if (needsStats) { - TaskExecutor taskExecutor = indexSearcher.getTaskExecutor(); - // build the term states concurrently - List> tasks = new ArrayList<>(context.leaves().size()); + PendingTermLookup[] pendingTermLookups = new PendingTermLookup[0]; for (LeafReaderContext ctx : context.leaves()) { - tasks.add( - () -> { - TermsEnum termsEnum = loadTermsEnum(ctx, term); - return termsEnum == null - ? null - : new TermStateInfo( - termsEnum.termState(), - ctx.ord, - termsEnum.docFreq(), - termsEnum.totalTermFreq()); - }); + Terms terms = Terms.getTerms(ctx.reader(), term.field()); + TermsEnum termsEnum = terms.iterator(); + // Schedule the I/O in the terms dictionary in the background. + IOBooleanSupplier termExistsSupplier = termsEnum.prepareSeekExact(term.bytes()); + if (termExistsSupplier != null) { + pendingTermLookups = ArrayUtil.grow(pendingTermLookups, ctx.ord + 1); + pendingTermLookups[ctx.ord] = new PendingTermLookup(termsEnum, termExistsSupplier); + } } - List resultInfos = taskExecutor.invokeAll(tasks); - for (TermStateInfo info : resultInfos) { - if (info != null) { + for (int ord = 0; ord < pendingTermLookups.length; ++ord) { + PendingTermLookup pendingTermLookup = pendingTermLookups[ord]; + if (pendingTermLookup != null && pendingTermLookup.supplier.get()) { + TermsEnum termsEnum = pendingTermLookup.termsEnum(); perReaderTermState.register( - info.getState(), info.getOrdinal(), info.getDocFreq(), info.getTotalTermFreq()); + termsEnum.termState(), ord, termsEnum.docFreq(), termsEnum.totalTermFreq()); } } } return perReaderTermState; } - private static TermsEnum loadTermsEnum(LeafReaderContext ctx, Term term) throws IOException { - final Terms terms = Terms.getTerms(ctx.reader(), term.field()); - final TermsEnum termsEnum = terms.iterator(); - if (termsEnum.seekExact(term.bytes())) { - return termsEnum; - } - return null; - } - /** Clears the {@link TermStates} internal state and removes all registered {@link TermState}s */ public void clear() { docFreq = 0; @@ -172,22 +161,60 @@ public final class TermStates { } /** - * Returns the {@link TermState} for a leaf reader context or null if no {@link - * TermState} for the context was registered. + * Returns a {@link Supplier} for a {@link TermState} for the given {@link LeafReaderContext}. + * This may return {@code null} if some cheap checks help figure out that this term doesn't exist + * in this leaf. The {@link Supplier} may then also return {@code null} if the term doesn't exist. + * + *

    Calling this method typically schedules some I/O in the background, so it is recommended to + * retrieve {@link Supplier}s across all required terms first before calling {@link Supplier#get} + * on all {@link Supplier}s so that the I/O for these terms can be performed in parallel. * * @param ctx the {@link LeafReaderContext} to get the {@link TermState} for. - * @return the {@link TermState} for the given readers ord or null if no {@link - * TermState} for the reader was registered + * @return a Supplier for a TermState. */ - public TermState get(LeafReaderContext ctx) throws IOException { + public IOSupplier get(LeafReaderContext ctx) throws IOException { assert ctx.ord >= 0 && ctx.ord < states.length; - if (term == null) return states[ctx.ord]; - if (this.states[ctx.ord] == null) { - TermsEnum te = loadTermsEnum(ctx, term); - this.states[ctx.ord] = te == null ? EMPTY_TERMSTATE : te.termState(); + if (term == null) { + if (states[ctx.ord] == null) { + return null; + } else { + return () -> states[ctx.ord]; + } } - if (this.states[ctx.ord] == EMPTY_TERMSTATE) return null; - return this.states[ctx.ord]; + if (this.states[ctx.ord] == null) { + final Terms terms = ctx.reader().terms(term.field()); + if (terms == null) { + this.states[ctx.ord] = EMPTY_TERMSTATE; + return null; + } + final TermsEnum termsEnum = terms.iterator(); + IOBooleanSupplier termExistsSupplier = termsEnum.prepareSeekExact(term.bytes()); + if (termExistsSupplier == null) { + this.states[ctx.ord] = EMPTY_TERMSTATE; + return null; + } + return () -> { + if (this.states[ctx.ord] == null) { + TermState state = null; + if (termExistsSupplier.get()) { + state = termsEnum.termState(); + this.states[ctx.ord] = state; + } else { + this.states[ctx.ord] = EMPTY_TERMSTATE; + } + } + TermState state = this.states[ctx.ord]; + if (state == EMPTY_TERMSTATE) { + return null; + } + return state; + }; + } + TermState state = this.states[ctx.ord]; + if (state == EMPTY_TERMSTATE) { + return null; + } + return () -> state; } /** @@ -230,40 +257,4 @@ public final class TermStates { return sb.toString(); } - - /** Wrapper over TermState, ordinal value, term doc frequency and total term frequency */ - private static final class TermStateInfo { - private final TermState state; - private final int ordinal; - private final int docFreq; - private final long totalTermFreq; - - /** Initialize TermStateInfo */ - public TermStateInfo(TermState state, int ordinal, int docFreq, long totalTermFreq) { - this.state = state; - this.ordinal = ordinal; - this.docFreq = docFreq; - this.totalTermFreq = totalTermFreq; - } - - /** Get term state */ - public TermState getState() { - return state; - } - - /** Get ordinal value */ - public int getOrdinal() { - return ordinal; - } - - /** Get term doc frequency */ - public int getDocFreq() { - return docFreq; - } - - /** Get total term frequency */ - public long getTotalTermFreq() { - return totalTermFreq; - } - } } diff --git a/lucene/core/src/java/org/apache/lucene/index/TermsEnum.java b/lucene/core/src/java/org/apache/lucene/index/TermsEnum.java index 79e985c9204..2ff6a2719b6 100644 --- a/lucene/core/src/java/org/apache/lucene/index/TermsEnum.java +++ b/lucene/core/src/java/org/apache/lucene/index/TermsEnum.java @@ -17,9 +17,11 @@ package org.apache.lucene.index; import java.io.IOException; +import org.apache.lucene.store.IndexInput; import org.apache.lucene.util.AttributeSource; import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.BytesRefIterator; +import org.apache.lucene.util.IOBooleanSupplier; /** * Iterator to seek ({@link #seekCeil(BytesRef)}, {@link #seekExact(BytesRef)}) or step through @@ -61,6 +63,23 @@ public abstract class TermsEnum implements BytesRefIterator { */ public abstract boolean seekExact(BytesRef text) throws IOException; + /** + * Two-phase {@link #seekExact}. The first phase typically calls {@link IndexInput#prefetch} on + * the right range of bytes under the hood, while the second phase {@link IOBooleanSupplier#get()} + * actually seeks the term within these bytes. This can be used to parallelize I/O across multiple + * terms by calling {@link #prepareSeekExact} on multiple terms enums before calling {@link + * IOBooleanSupplier#get()}. + * + *

    NOTE: It is illegal to call other methods on this {@link TermsEnum} after calling + * this method until {@link IOBooleanSupplier#get()} is called. + * + *

    NOTE: This may return {@code null} if this {@link TermsEnum} can identify that the + * term may not exist without performing any I/O. + * + *

    NOTE: The returned {@link IOBooleanSupplier} must be consumed in the same thread. + */ + public abstract IOBooleanSupplier prepareSeekExact(BytesRef text) throws IOException; + /** * Seeks to the specified term, if it exists, or to the next (ceiling) term. Returns SeekStatus to * indicate whether exact term was found, a different term was found, or EOF was hit. The target @@ -178,9 +197,7 @@ public abstract class TermsEnum implements BytesRefIterator { * of unused Attributes does not matter. */ public static final TermsEnum EMPTY = - new TermsEnum() { - - private AttributeSource atts = null; + new BaseTermsEnum() { @Override public SeekStatus seekCeil(BytesRef term) { @@ -225,19 +242,6 @@ public abstract class TermsEnum implements BytesRefIterator { return null; } - @Override // make it synchronized here, to prevent double lazy init - public synchronized AttributeSource attributes() { - if (atts == null) { - atts = new AttributeSource(); - } - return atts; - } - - @Override - public boolean seekExact(BytesRef text) throws IOException { - return seekCeil(text) == SeekStatus.FOUND; - } - @Override public TermState termState() { throw new IllegalStateException("this method should never be called"); diff --git a/lucene/core/src/java/org/apache/lucene/search/BlendedTermQuery.java b/lucene/core/src/java/org/apache/lucene/search/BlendedTermQuery.java index 8b7d3e80fcd..8dc66036969 100644 --- a/lucene/core/src/java/org/apache/lucene/search/BlendedTermQuery.java +++ b/lucene/core/src/java/org/apache/lucene/search/BlendedTermQuery.java @@ -26,6 +26,7 @@ import org.apache.lucene.index.TermState; import org.apache.lucene.index.TermStates; import org.apache.lucene.search.BooleanClause.Occur; import org.apache.lucene.util.ArrayUtil; +import org.apache.lucene.util.IOSupplier; import org.apache.lucene.util.InPlaceMergeSorter; /** @@ -316,7 +317,11 @@ public final class BlendedTermQuery extends Query { List leaves = readerContext.leaves(); TermStates newCtx = new TermStates(readerContext); for (int i = 0; i < leaves.size(); ++i) { - TermState termState = ctx.get(leaves.get(i)); + IOSupplier supplier = ctx.get(leaves.get(i)); + if (supplier == null) { + continue; + } + TermState termState = supplier.get(); if (termState == null) { continue; } diff --git a/lucene/core/src/java/org/apache/lucene/search/FuzzyTermsEnum.java b/lucene/core/src/java/org/apache/lucene/search/FuzzyTermsEnum.java index cfd6ed232de..bf9aa0f8f2c 100644 --- a/lucene/core/src/java/org/apache/lucene/search/FuzzyTermsEnum.java +++ b/lucene/core/src/java/org/apache/lucene/search/FuzzyTermsEnum.java @@ -18,6 +18,7 @@ package org.apache.lucene.search; import java.io.IOException; import java.util.function.Supplier; +import org.apache.lucene.index.BaseTermsEnum; import org.apache.lucene.index.ImpactsEnum; import org.apache.lucene.index.PostingsEnum; import org.apache.lucene.index.Term; @@ -30,6 +31,7 @@ import org.apache.lucene.util.AttributeReflector; import org.apache.lucene.util.AttributeSource; import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.BytesRefBuilder; +import org.apache.lucene.util.IOBooleanSupplier; import org.apache.lucene.util.UnicodeUtil; import org.apache.lucene.util.automaton.CompiledAutomaton; @@ -39,7 +41,7 @@ import org.apache.lucene.util.automaton.CompiledAutomaton; *

    Term enumerations are always ordered by {@link BytesRef#compareTo}. Each term in the * enumeration is greater than all that precede it. */ -public final class FuzzyTermsEnum extends TermsEnum { +public final class FuzzyTermsEnum extends BaseTermsEnum { // NOTE: we can't subclass FilteredTermsEnum here because we need to sometimes change actualEnum: private TermsEnum actualEnum; @@ -324,6 +326,11 @@ public final class FuzzyTermsEnum extends TermsEnum { return actualEnum.seekExact(text); } + @Override + public IOBooleanSupplier prepareSeekExact(BytesRef text) throws IOException { + return actualEnum.prepareSeekExact(text); + } + @Override public SeekStatus seekCeil(BytesRef text) throws IOException { return actualEnum.seekCeil(text); diff --git a/lucene/core/src/java/org/apache/lucene/search/MultiPhraseQuery.java b/lucene/core/src/java/org/apache/lucene/search/MultiPhraseQuery.java index 315b94a2acd..83510c02991 100644 --- a/lucene/core/src/java/org/apache/lucene/search/MultiPhraseQuery.java +++ b/lucene/core/src/java/org/apache/lucene/search/MultiPhraseQuery.java @@ -38,6 +38,7 @@ import org.apache.lucene.search.similarities.Similarity; import org.apache.lucene.search.similarities.Similarity.SimScorer; import org.apache.lucene.util.ArrayUtil; import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.IOSupplier; import org.apache.lucene.util.PriorityQueue; /** @@ -271,7 +272,8 @@ public class MultiPhraseQuery extends Query { List postings = new ArrayList<>(); for (Term term : terms) { - TermState termState = termStates.get(term).get(context); + IOSupplier supplier = termStates.get(term).get(context); + TermState termState = supplier == null ? null : supplier.get(); if (termState != null) { termsEnum.seekExact(term.bytes(), termState); postings.add( diff --git a/lucene/core/src/java/org/apache/lucene/search/PhraseQuery.java b/lucene/core/src/java/org/apache/lucene/search/PhraseQuery.java index c5a5ee36fd4..55d0e228d1a 100644 --- a/lucene/core/src/java/org/apache/lucene/search/PhraseQuery.java +++ b/lucene/core/src/java/org/apache/lucene/search/PhraseQuery.java @@ -38,6 +38,7 @@ import org.apache.lucene.search.similarities.Similarity; import org.apache.lucene.search.similarities.Similarity.SimScorer; import org.apache.lucene.util.ArrayUtil; import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.IOSupplier; /** * A Query that matches documents containing a particular sequence of terms. A PhraseQuery is built @@ -498,7 +499,8 @@ public class PhraseQuery extends Query { for (int i = 0; i < terms.length; i++) { final Term t = terms[i]; - final TermState state = states[i].get(context); + final IOSupplier supplier = states[i].get(context); + final TermState state = supplier == null ? null : supplier.get(); if (state == null) { /* term doesnt exist in this segment */ assert termNotInReader(reader, t) : "no termstate found but term exists in reader"; diff --git a/lucene/core/src/java/org/apache/lucene/search/SynonymQuery.java b/lucene/core/src/java/org/apache/lucene/search/SynonymQuery.java index 0500f4630dc..82a3b6e0148 100644 --- a/lucene/core/src/java/org/apache/lucene/search/SynonymQuery.java +++ b/lucene/core/src/java/org/apache/lucene/search/SynonymQuery.java @@ -17,6 +17,7 @@ package org.apache.lucene.search; import java.io.IOException; +import java.io.UncheckedIOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -38,6 +39,7 @@ import org.apache.lucene.index.Terms; import org.apache.lucene.index.TermsEnum; import org.apache.lucene.search.similarities.Similarity; import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.IOSupplier; import org.apache.lucene.util.PriorityQueue; /** @@ -277,80 +279,120 @@ public final class SynonymQuery extends Query { @Override public ScorerSupplier scorerSupplier(LeafReaderContext context) throws IOException { - final Scorer synonymScorer; - List iterators = new ArrayList<>(); - List impacts = new ArrayList<>(); - List termBoosts = new ArrayList<>(); + @SuppressWarnings({"rawtypes", "unchecked"}) + IOSupplier[] termStateSuppliers = new IOSupplier[terms.length]; for (int i = 0; i < terms.length; i++) { - TermState state = termStates[i].get(context); - if (state != null) { - TermsEnum termsEnum = context.reader().terms(field).iterator(); - termsEnum.seekExact(terms[i].term, state); - if (scoreMode == ScoreMode.TOP_SCORES) { - ImpactsEnum impactsEnum = termsEnum.impacts(PostingsEnum.FREQS); - iterators.add(impactsEnum); - impacts.add(impactsEnum); - } else { - PostingsEnum postingsEnum = termsEnum.postings(null, PostingsEnum.FREQS); - iterators.add(postingsEnum); - impacts.add(new SlowImpactsEnum(postingsEnum)); + // schedule the I/O for terms dictionary lookups in the background + termStateSuppliers[i] = termStates[i].get(context); + } + + return new ScorerSupplier() { + + List iterators; + List impacts; + List termBoosts; + long cost; + + private void init() throws IOException { + if (iterators != null) { + return; + } + iterators = new ArrayList<>(); + impacts = new ArrayList<>(); + termBoosts = new ArrayList<>(); + cost = 0L; + + for (int i = 0; i < terms.length; i++) { + IOSupplier supplier = termStateSuppliers[i]; + TermState state = supplier == null ? null : supplier.get(); + if (state != null) { + TermsEnum termsEnum = context.reader().terms(field).iterator(); + termsEnum.seekExact(terms[i].term, state); + if (scoreMode == ScoreMode.TOP_SCORES) { + ImpactsEnum impactsEnum = termsEnum.impacts(PostingsEnum.FREQS); + iterators.add(impactsEnum); + impacts.add(impactsEnum); + } else { + PostingsEnum postingsEnum = termsEnum.postings(null, PostingsEnum.FREQS); + iterators.add(postingsEnum); + impacts.add(new SlowImpactsEnum(postingsEnum)); + } + termBoosts.add(terms[i].boost); + } + } + + for (DocIdSetIterator iterator : iterators) { + cost += iterator.cost(); } - termBoosts.add(terms[i].boost); } - } - if (iterators.isEmpty()) { - return null; - } + @Override + public Scorer get(long leadCost) throws IOException { + init(); - LeafSimScorer simScorer = new LeafSimScorer(simWeight, context.reader(), field, true); + if (iterators.isEmpty()) { + return new ConstantScoreScorer(0f, scoreMode, DocIdSetIterator.empty()); + } - // we must optimize this case (term not in segment), disjunctions require >= 2 subs - if (iterators.size() == 1) { - final TermScorer scorer; - if (scoreMode == ScoreMode.TOP_SCORES) { - scorer = new TermScorer(impacts.get(0), simScorer); - } else { - scorer = new TermScorer(iterators.get(0), simScorer); - } - float boost = termBoosts.get(0); - synonymScorer = - scoreMode == ScoreMode.COMPLETE_NO_SCORES || boost == 1f + LeafSimScorer simScorer = new LeafSimScorer(simWeight, context.reader(), field, true); + + // we must optimize this case (term not in segment), disjunctions require >= 2 subs + if (iterators.size() == 1) { + final TermScorer scorer; + if (scoreMode == ScoreMode.TOP_SCORES) { + scorer = new TermScorer(impacts.get(0), simScorer); + } else { + scorer = new TermScorer(iterators.get(0), simScorer); + } + float boost = termBoosts.get(0); + return scoreMode == ScoreMode.COMPLETE_NO_SCORES || boost == 1f ? scorer : new FreqBoostTermScorer(boost, scorer, simScorer); - } else { + } else { - // we use termscorers + disjunction as an impl detail - DisiPriorityQueue queue = new DisiPriorityQueue(iterators.size()); - for (int i = 0; i < iterators.size(); i++) { - PostingsEnum postings = iterators.get(i); - final TermScorer termScorer = new TermScorer(postings, simScorer); - float boost = termBoosts.get(i); - final DisiWrapperFreq wrapper = new DisiWrapperFreq(termScorer, boost); - queue.add(wrapper); - } - // Even though it is called approximation, it is accurate since none of - // the sub iterators are two-phase iterators. - DocIdSetIterator iterator = new DisjunctionDISIApproximation(queue); + // we use termscorers + disjunction as an impl detail + DisiPriorityQueue queue = new DisiPriorityQueue(iterators.size()); + for (int i = 0; i < iterators.size(); i++) { + PostingsEnum postings = iterators.get(i); + final TermScorer termScorer = new TermScorer(postings, simScorer); + float boost = termBoosts.get(i); + final DisiWrapperFreq wrapper = new DisiWrapperFreq(termScorer, boost); + queue.add(wrapper); + } + // Even though it is called approximation, it is accurate since none of + // the sub iterators are two-phase iterators. + DocIdSetIterator iterator = new DisjunctionDISIApproximation(queue); - float[] boosts = new float[impacts.size()]; - for (int i = 0; i < boosts.length; i++) { - boosts[i] = termBoosts.get(i); - } - ImpactsSource impactsSource = mergeImpacts(impacts.toArray(new ImpactsEnum[0]), boosts); - MaxScoreCache maxScoreCache = new MaxScoreCache(impactsSource, simScorer.getSimScorer()); - ImpactsDISI impactsDisi = new ImpactsDISI(iterator, maxScoreCache); + float[] boosts = new float[impacts.size()]; + for (int i = 0; i < boosts.length; i++) { + boosts[i] = termBoosts.get(i); + } + ImpactsSource impactsSource = mergeImpacts(impacts.toArray(new ImpactsEnum[0]), boosts); + MaxScoreCache maxScoreCache = + new MaxScoreCache(impactsSource, simScorer.getSimScorer()); + ImpactsDISI impactsDisi = new ImpactsDISI(iterator, maxScoreCache); - if (scoreMode == ScoreMode.TOP_SCORES) { - // TODO: only do this when this is the top-level scoring clause - // (ScorerSupplier#setTopLevelScoringClause) to save the overhead of wrapping with - // ImpactsDISI when it would not help - iterator = impactsDisi; + if (scoreMode == ScoreMode.TOP_SCORES) { + // TODO: only do this when this is the top-level scoring clause + // (ScorerSupplier#setTopLevelScoringClause) to save the overhead of wrapping with + // ImpactsDISI when it would not help + iterator = impactsDisi; + } + + return new SynonymScorer(queue, iterator, impactsDisi, simScorer); + } } - synonymScorer = new SynonymScorer(queue, iterator, impactsDisi, simScorer); - } - return new DefaultScorerSupplier(synonymScorer); + @Override + public long cost() { + try { + init(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return cost; + } + }; } @Override diff --git a/lucene/core/src/java/org/apache/lucene/search/TermQuery.java b/lucene/core/src/java/org/apache/lucene/search/TermQuery.java index 84037acd0d4..3a843addcc3 100644 --- a/lucene/core/src/java/org/apache/lucene/search/TermQuery.java +++ b/lucene/core/src/java/org/apache/lucene/search/TermQuery.java @@ -17,6 +17,7 @@ package org.apache.lucene.search; import java.io.IOException; +import java.io.UncheckedIOException; import java.util.Objects; import org.apache.lucene.index.IndexReaderContext; import org.apache.lucene.index.LeafReader; @@ -28,6 +29,7 @@ import org.apache.lucene.index.TermState; import org.apache.lucene.index.TermStates; import org.apache.lucene.index.TermsEnum; import org.apache.lucene.search.similarities.Similarity; +import org.apache.lucene.util.IOSupplier; /** * A Query that matches documents containing a term. This may be combined with other terms with a @@ -119,18 +121,35 @@ public class TermQuery extends Query { : "The top-reader used to create Weight is not the same as the current reader's top-reader (" + ReaderUtil.getTopLevelContext(context); - final TermsEnum termsEnum = getTermsEnum(context); - if (termsEnum == null) { + final IOSupplier stateSupplier = termStates.get(context); + if (stateSupplier == null) { return null; } - final int docFreq = termsEnum.docFreq(); return new ScorerSupplier() { + private TermsEnum termsEnum; private boolean topLevelScoringClause = false; + private TermsEnum getTermsEnum() throws IOException { + if (termsEnum == null) { + TermState state = stateSupplier.get(); + if (state == null) { + return null; + } + termsEnum = context.reader().terms(term.field()).iterator(); + termsEnum.seekExact(term.bytes(), state); + } + return termsEnum; + } + @Override public Scorer get(long leadCost) throws IOException { + TermsEnum termsEnum = getTermsEnum(); + if (termsEnum == null) { + return new ConstantScoreScorer(0f, scoreMode, DocIdSetIterator.empty()); + } + LeafSimScorer scorer = new LeafSimScorer(simScorer, context.reader(), term.field(), scoreMode.needsScores()); if (scoreMode == ScoreMode.TOP_SCORES) { @@ -149,7 +168,12 @@ public class TermQuery extends Query { @Override public long cost() { - return docFreq; + try { + TermsEnum te = getTermsEnum(); + return te == null ? 0 : te.docFreq(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } @Override @@ -173,7 +197,8 @@ public class TermQuery extends Query { assert termStates.wasBuiltFor(ReaderUtil.getTopLevelContext(context)) : "The top-reader used to create Weight is not the same as the current reader's top-reader (" + ReaderUtil.getTopLevelContext(context); - final TermState state = termStates.get(context); + final IOSupplier supplier = termStates.get(context); + final TermState state = supplier == null ? null : supplier.get(); if (state == null) { // term is not present in that reader assert termNotInReader(context.reader(), term) : "no termstate found but term exists in reader term=" + term; @@ -193,11 +218,11 @@ public class TermQuery extends Query { @Override public Explanation explain(LeafReaderContext context, int doc) throws IOException { - TermScorer scorer = (TermScorer) scorer(context); + Scorer scorer = scorer(context); if (scorer != null) { int newDoc = scorer.iterator().advance(doc); if (newDoc == doc) { - float freq = scorer.freq(); + float freq = ((TermScorer) scorer).freq(); LeafSimScorer docScorer = new LeafSimScorer(simScorer, context.reader(), term.field(), true); Explanation freqExplanation = diff --git a/lucene/core/src/java/org/apache/lucene/util/IOBooleanSupplier.java b/lucene/core/src/java/org/apache/lucene/util/IOBooleanSupplier.java new file mode 100644 index 00000000000..4100c6c53c5 --- /dev/null +++ b/lucene/core/src/java/org/apache/lucene/util/IOBooleanSupplier.java @@ -0,0 +1,37 @@ +/* + * 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.util; + +import java.io.IOException; + +/** + * Boolean supplier that is allowed to throw an IOException. + * + * @see java.util.function.BooleanSupplier + */ +@FunctionalInterface +public interface IOBooleanSupplier { + + /** + * Gets the boolean result. + * + * @return the result + * @throws IOException if supplying the result throws an {@link IOException} + */ + boolean get() throws IOException; +} diff --git a/lucene/core/src/test/org/apache/lucene/search/TestBooleanRewrites.java b/lucene/core/src/test/org/apache/lucene/search/TestBooleanRewrites.java index 87b8068d2f1..15f0c0d0c94 100644 --- a/lucene/core/src/test/org/apache/lucene/search/TestBooleanRewrites.java +++ b/lucene/core/src/test/org/apache/lucene/search/TestBooleanRewrites.java @@ -92,7 +92,7 @@ public class TestBooleanRewrites extends LuceneTestCase { // make sure to set score=0 BooleanQuery.Builder query2 = new BooleanQuery.Builder(); query2.add(new TermQuery(new Term("field", "a")), Occur.FILTER); - query2.add(new TermQuery(new Term("field", "b")), Occur.SHOULD); + query2.add(new TermQuery(new Term("missing_field", "b")), Occur.SHOULD); final Weight weight = searcher.createWeight(searcher.rewrite(query2.build()), ScoreMode.COMPLETE, 1); final Scorer scorer = weight.scorer(reader.leaves().get(0)); diff --git a/lucene/core/src/test/org/apache/lucene/search/TestBooleanScorer.java b/lucene/core/src/test/org/apache/lucene/search/TestBooleanScorer.java index fec70cb3dcc..1258dd8b10e 100644 --- a/lucene/core/src/test/org/apache/lucene/search/TestBooleanScorer.java +++ b/lucene/core/src/test/org/apache/lucene/search/TestBooleanScorer.java @@ -180,7 +180,7 @@ public class TestBooleanScorer extends LuceneTestCase { Query query = new BooleanQuery.Builder() .add(new TermQuery(new Term("foo", "bar")), Occur.SHOULD) // existing term - .add(new TermQuery(new Term("foo", "baz")), Occur.SHOULD) // missing term + .add(new TermQuery(new Term("missing_field", "baz")), Occur.SHOULD) // missing term .build(); // no scores -> term scorer diff --git a/lucene/core/src/test/org/apache/lucene/search/TestTermQuery.java b/lucene/core/src/test/org/apache/lucene/search/TestTermQuery.java index 3b35f67cad1..8911500546b 100644 --- a/lucene/core/src/test/org/apache/lucene/search/TestTermQuery.java +++ b/lucene/core/src/test/org/apache/lucene/search/TestTermQuery.java @@ -43,6 +43,7 @@ import org.apache.lucene.tests.search.QueryUtils; import org.apache.lucene.tests.util.LuceneTestCase; import org.apache.lucene.tests.util.TestUtil; import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.IOBooleanSupplier; import org.apache.lucene.util.IOUtils; public class TestTermQuery extends LuceneTestCase { @@ -259,6 +260,11 @@ public class TestTermQuery extends LuceneTestCase { throw new AssertionError("no seek"); } + @Override + public IOBooleanSupplier prepareSeekExact(BytesRef text) throws IOException { + throw new AssertionError("no seek"); + } + @Override public void seekExact(BytesRef term, TermState state) throws IOException { throw new AssertionError("no seek"); diff --git a/lucene/join/src/test/org/apache/lucene/search/join/TestBlockJoin.java b/lucene/join/src/test/org/apache/lucene/search/join/TestBlockJoin.java index 4e445d7eeb2..77f00818dec 100644 --- a/lucene/join/src/test/org/apache/lucene/search/join/TestBlockJoin.java +++ b/lucene/join/src/test/org/apache/lucene/search/join/TestBlockJoin.java @@ -1380,7 +1380,7 @@ public class TestBlockJoin extends LuceneTestCase { IndexSearcher searcher = newSearcher(r); // never matches: - Query childQuery = new TermQuery(new Term("childText", "bogus")); + Query childQuery = new TermQuery(new Term("childBogusField", "bogus")); BitSetProducer parentsFilter = new QueryBitSetProducer(new TermQuery(new Term("isParent", "yes"))); CheckJoinIndex.check(r, parentsFilter); diff --git a/lucene/queries/src/java/org/apache/lucene/queries/spans/SpanTermQuery.java b/lucene/queries/src/java/org/apache/lucene/queries/spans/SpanTermQuery.java index 39081eb48c1..359f6ae7e95 100644 --- a/lucene/queries/src/java/org/apache/lucene/queries/spans/SpanTermQuery.java +++ b/lucene/queries/src/java/org/apache/lucene/queries/spans/SpanTermQuery.java @@ -32,6 +32,7 @@ import org.apache.lucene.index.TermsEnum; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.QueryVisitor; import org.apache.lucene.search.ScoreMode; +import org.apache.lucene.util.IOSupplier; /** * Matches spans containing a term. This should not be used for terms that are indexed at position @@ -135,7 +136,8 @@ public class SpanTermQuery extends SpanQuery { : "The top-reader used to create Weight is not the same as the current reader's top-reader (" + ReaderUtil.getTopLevelContext(context); - final TermState state = termStates.get(context); + final IOSupplier supplier = termStates.get(context); + final TermState state = supplier == null ? null : supplier.get(); if (state == null) { // term is not present in that reader assert context.reader().docFreq(term) == 0 : "no termstate found but term exists in reader term=" + term; diff --git a/lucene/sandbox/src/java/org/apache/lucene/sandbox/search/CombinedFieldQuery.java b/lucene/sandbox/src/java/org/apache/lucene/sandbox/search/CombinedFieldQuery.java index 9fe6b890044..35b9e8dc78d 100644 --- a/lucene/sandbox/src/java/org/apache/lucene/sandbox/search/CombinedFieldQuery.java +++ b/lucene/sandbox/src/java/org/apache/lucene/sandbox/search/CombinedFieldQuery.java @@ -62,6 +62,7 @@ import org.apache.lucene.search.similarities.Similarity; import org.apache.lucene.search.similarities.SimilarityBase; import org.apache.lucene.util.Accountable; import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.IOSupplier; import org.apache.lucene.util.RamUsageEstimator; import org.apache.lucene.util.SmallFloat; @@ -405,7 +406,8 @@ public final class CombinedFieldQuery extends Query implements Accountable { List iterators = new ArrayList<>(); List fields = new ArrayList<>(); for (int i = 0; i < fieldTerms.length; i++) { - TermState state = termStates[i].get(context); + IOSupplier supplier = termStates[i].get(context); + TermState state = supplier == null ? null : supplier.get(); if (state != null) { TermsEnum termsEnum = context.reader().terms(fieldTerms[i].field()).iterator(); termsEnum.seekExact(fieldTerms[i].bytes(), state); diff --git a/lucene/sandbox/src/java/org/apache/lucene/sandbox/search/PhraseWildcardQuery.java b/lucene/sandbox/src/java/org/apache/lucene/sandbox/search/PhraseWildcardQuery.java index 2ba011dbf3a..49d6ef4bd9a 100644 --- a/lucene/sandbox/src/java/org/apache/lucene/sandbox/search/PhraseWildcardQuery.java +++ b/lucene/sandbox/src/java/org/apache/lucene/sandbox/search/PhraseWildcardQuery.java @@ -56,6 +56,7 @@ import org.apache.lucene.search.Weight; import org.apache.lucene.search.similarities.Similarity; import org.apache.lucene.util.ArrayUtil; import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.IOSupplier; import org.apache.lucene.util.mutable.MutableValueBool; /** @@ -387,7 +388,8 @@ public class PhraseWildcardQuery extends Query { Terms terms = leafReaderContext.reader().terms(term.field()); if (terms != null) { checkTermsHavePositions(terms); - TermState termState = termStates.get(leafReaderContext); + IOSupplier supplier = termStates.get(leafReaderContext); + TermState termState = supplier == null ? null : supplier.get(); if (termState != null) { termMatchesInSegment = true; numMatches++; diff --git a/lucene/sandbox/src/java/org/apache/lucene/sandbox/search/TermAutomatonQuery.java b/lucene/sandbox/src/java/org/apache/lucene/sandbox/search/TermAutomatonQuery.java index 1e8855dfda3..46386b52249 100644 --- a/lucene/sandbox/src/java/org/apache/lucene/sandbox/search/TermAutomatonQuery.java +++ b/lucene/sandbox/src/java/org/apache/lucene/sandbox/search/TermAutomatonQuery.java @@ -50,6 +50,7 @@ import org.apache.lucene.search.Weight; import org.apache.lucene.search.similarities.Similarity; import org.apache.lucene.util.Accountable; import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.IOSupplier; import org.apache.lucene.util.IntsRef; import org.apache.lucene.util.RamUsageEstimator; import org.apache.lucene.util.automaton.Automaton; @@ -416,7 +417,8 @@ public class TermAutomatonQuery extends Query implements Accountable { : "The top-reader used to create Weight is not the same as the current reader's top-reader (" + ReaderUtil.getTopLevelContext(context); BytesRef term = idToTerm.get(ent.key); - TermState state = termStates.get(context); + IOSupplier supplier = termStates.get(context); + TermState state = supplier == null ? null : supplier.get(); if (state != null) { TermsEnum termsEnum = context.reader().terms(field).iterator(); termsEnum.seekExact(term, state); diff --git a/lucene/test-framework/src/java/org/apache/lucene/tests/index/AssertingLeafReader.java b/lucene/test-framework/src/java/org/apache/lucene/tests/index/AssertingLeafReader.java index fc02ae82de2..3151754a075 100644 --- a/lucene/test-framework/src/java/org/apache/lucene/tests/index/AssertingLeafReader.java +++ b/lucene/test-framework/src/java/org/apache/lucene/tests/index/AssertingLeafReader.java @@ -51,6 +51,7 @@ import org.apache.lucene.internal.tests.TestSecrets; import org.apache.lucene.search.DocIdSetIterator; import org.apache.lucene.util.Bits; import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.IOBooleanSupplier; import org.apache.lucene.util.VirtualMethod; import org.apache.lucene.util.automaton.CompiledAutomaton; @@ -267,7 +268,8 @@ public class AssertingLeafReader extends FilterLeafReader { private enum State { INITIAL, POSITIONED, - UNPOSITIONED + UNPOSITIONED, + TWO_PHASE_SEEKING; }; private State state = State.INITIAL; @@ -370,6 +372,7 @@ public class AssertingLeafReader extends FilterLeafReader { @Override public void seekExact(long ord) throws IOException { assertThread("Terms enums", creationThread); + assert state != State.TWO_PHASE_SEEKING : "Unfinished two-phase seeking"; super.seekExact(ord); state = State.POSITIONED; } @@ -377,6 +380,7 @@ public class AssertingLeafReader extends FilterLeafReader { @Override public SeekStatus seekCeil(BytesRef term) throws IOException { assertThread("Terms enums", creationThread); + assert state != State.TWO_PHASE_SEEKING : "Unfinished two-phase seeking"; assert term.isValid(); SeekStatus result = super.seekCeil(term); if (result == SeekStatus.END) { @@ -390,6 +394,7 @@ public class AssertingLeafReader extends FilterLeafReader { @Override public boolean seekExact(BytesRef text) throws IOException { assertThread("Terms enums", creationThread); + assert state != State.TWO_PHASE_SEEKING : "Unfinished two-phase seeking"; assert text.isValid(); boolean result; if (delegateOverridesSeekExact) { @@ -405,6 +410,27 @@ public class AssertingLeafReader extends FilterLeafReader { return result; } + @Override + public IOBooleanSupplier prepareSeekExact(BytesRef text) throws IOException { + assertThread("Terms enums", creationThread); + assert state != State.TWO_PHASE_SEEKING : "Unfinished two-phase seeking"; + assert text.isValid(); + IOBooleanSupplier in = this.in.prepareSeekExact(text); + if (in == null) { + return null; + } + state = State.TWO_PHASE_SEEKING; + return () -> { + boolean exists = in.get(); + if (exists) { + state = State.POSITIONED; + } else { + state = State.UNPOSITIONED; + } + return exists; + }; + } + @Override public TermState termState() throws IOException { assertThread("Terms enums", creationThread); @@ -415,6 +441,7 @@ public class AssertingLeafReader extends FilterLeafReader { @Override public void seekExact(BytesRef term, TermState state) throws IOException { assertThread("Terms enums", creationThread); + assert this.state != State.TWO_PHASE_SEEKING : "Unfinished two-phase seeking"; assert term.isValid(); in.seekExact(term, state); this.state = State.POSITIONED; From 428fdb529117e107d5fa225d8ec23360e1225c02 Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Wed, 10 Jul 2024 10:28:48 -0400 Subject: [PATCH 41/42] Reduce heap usage for knn index writers (#13538) * Reduce heap usage for knn index writers * iter * fixing heap usage & adding changes * javadocs --- lucene/CHANGES.txt | 2 + .../codecs/hnsw/FlatFieldVectorsWriter.java | 28 ++-- .../lucene/codecs/hnsw/FlatVectorsWriter.java | 12 +- .../lucene99/Lucene99FlatVectorsWriter.java | 58 +++++--- .../lucene99/Lucene99HnswVectorsWriter.java | 79 +++++++---- .../Lucene99ScalarQuantizedVectorsWriter.java | 132 ++++++++++-------- 6 files changed, 183 insertions(+), 128 deletions(-) diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index b75eaa73ebf..26a7c06e483 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -280,6 +280,8 @@ Optimizations * GITHUB#13175: Stop double-checking priority queue inserts in some FacetCount classes. (Jakub Slowinski) +* GITHUB#13538: Slightly reduce heap usage for HNSW and scalar quantized vector writers. (Ben Trent) + Changes in runtime behavior --------------------- diff --git a/lucene/core/src/java/org/apache/lucene/codecs/hnsw/FlatFieldVectorsWriter.java b/lucene/core/src/java/org/apache/lucene/codecs/hnsw/FlatFieldVectorsWriter.java index 313ccccd4eb..fc71bb729db 100644 --- a/lucene/core/src/java/org/apache/lucene/codecs/hnsw/FlatFieldVectorsWriter.java +++ b/lucene/core/src/java/org/apache/lucene/codecs/hnsw/FlatFieldVectorsWriter.java @@ -17,7 +17,10 @@ package org.apache.lucene.codecs.hnsw; +import java.io.IOException; +import java.util.List; import org.apache.lucene.codecs.KnnFieldVectorsWriter; +import org.apache.lucene.index.DocsWithFieldSet; /** * Vectors' writer for a field @@ -26,20 +29,25 @@ import org.apache.lucene.codecs.KnnFieldVectorsWriter; * @lucene.experimental */ public abstract class FlatFieldVectorsWriter extends KnnFieldVectorsWriter { - /** - * The delegate to write to, can be null When non-null, all vectors seen should be written to the - * delegate along with being written to the flat vectors. + * @return a list of vectors to be written */ - protected final KnnFieldVectorsWriter indexingDelegate; + public abstract List getVectors(); /** - * Sole constructor that expects some indexingDelegate. All vectors seen should be written to the - * delegate along with being written to the flat vectors. + * @return the docsWithFieldSet for the field writer + */ + public abstract DocsWithFieldSet getDocsWithFieldSet(); + + /** + * indicates that this writer is done and no new vectors are allowed to be added * - * @param indexingDelegate the delegate to write to, can be null + * @throws IOException if an I/O error occurs */ - protected FlatFieldVectorsWriter(KnnFieldVectorsWriter indexingDelegate) { - this.indexingDelegate = indexingDelegate; - } + public abstract void finish() throws IOException; + + /** + * @return true if the writer is done and no new vectors are allowed to be added + */ + public abstract boolean isFinished(); } diff --git a/lucene/core/src/java/org/apache/lucene/codecs/hnsw/FlatVectorsWriter.java b/lucene/core/src/java/org/apache/lucene/codecs/hnsw/FlatVectorsWriter.java index 3a7803011aa..37c4f546bab 100644 --- a/lucene/core/src/java/org/apache/lucene/codecs/hnsw/FlatVectorsWriter.java +++ b/lucene/core/src/java/org/apache/lucene/codecs/hnsw/FlatVectorsWriter.java @@ -18,7 +18,6 @@ package org.apache.lucene.codecs.hnsw; import java.io.IOException; -import org.apache.lucene.codecs.KnnFieldVectorsWriter; import org.apache.lucene.codecs.KnnVectorsWriter; import org.apache.lucene.index.FieldInfo; import org.apache.lucene.index.MergeState; @@ -46,21 +45,14 @@ public abstract class FlatVectorsWriter extends KnnVectorsWriter { } /** - * Add a new field for indexing, allowing the user to provide a writer that the flat vectors - * writer can delegate to if additional indexing logic is required. + * Add a new field for indexing * * @param fieldInfo fieldInfo of the field to add - * @param indexWriter the writer to delegate to, can be null * @return a writer for the field * @throws IOException if an I/O error occurs when adding the field */ - public abstract FlatFieldVectorsWriter addField( - FieldInfo fieldInfo, KnnFieldVectorsWriter indexWriter) throws IOException; - @Override - public FlatFieldVectorsWriter addField(FieldInfo fieldInfo) throws IOException { - return addField(fieldInfo, null); - } + public abstract FlatFieldVectorsWriter addField(FieldInfo fieldInfo) throws IOException; /** * Write the field for merging, providing a scorer over the newly merged flat vectors. This way diff --git a/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99FlatVectorsWriter.java b/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99FlatVectorsWriter.java index b80c9f4d7f5..5643752796c 100644 --- a/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99FlatVectorsWriter.java +++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99FlatVectorsWriter.java @@ -27,7 +27,6 @@ import java.nio.ByteOrder; import java.util.ArrayList; import java.util.List; import org.apache.lucene.codecs.CodecUtil; -import org.apache.lucene.codecs.KnnFieldVectorsWriter; import org.apache.lucene.codecs.KnnVectorsWriter; import org.apache.lucene.codecs.hnsw.FlatFieldVectorsWriter; import org.apache.lucene.codecs.hnsw.FlatVectorsScorer; @@ -111,18 +110,12 @@ public final class Lucene99FlatVectorsWriter extends FlatVectorsWriter { } @Override - public FlatFieldVectorsWriter addField( - FieldInfo fieldInfo, KnnFieldVectorsWriter indexWriter) throws IOException { - FieldWriter newField = FieldWriter.create(fieldInfo, indexWriter); + public FlatFieldVectorsWriter addField(FieldInfo fieldInfo) throws IOException { + FieldWriter newField = FieldWriter.create(fieldInfo); fields.add(newField); return newField; } - @Override - public FlatFieldVectorsWriter addField(FieldInfo fieldInfo) throws IOException { - return addField(fieldInfo, null); - } - @Override public void flush(int maxDoc, Sorter.DocMap sortMap) throws IOException { for (FieldWriter field : fields) { @@ -131,6 +124,7 @@ public final class Lucene99FlatVectorsWriter extends FlatVectorsWriter { } else { writeSortingField(field, maxDoc, sortMap); } + field.finish(); } } @@ -403,22 +397,20 @@ public final class Lucene99FlatVectorsWriter extends FlatVectorsWriter { private final int dim; private final DocsWithFieldSet docsWithField; private final List vectors; + private boolean finished; private int lastDocID = -1; - @SuppressWarnings("unchecked") - static FieldWriter create(FieldInfo fieldInfo, KnnFieldVectorsWriter indexWriter) { + static FieldWriter create(FieldInfo fieldInfo) { int dim = fieldInfo.getVectorDimension(); return switch (fieldInfo.getVectorEncoding()) { - case BYTE -> new Lucene99FlatVectorsWriter.FieldWriter<>( - fieldInfo, (KnnFieldVectorsWriter) indexWriter) { + case BYTE -> new Lucene99FlatVectorsWriter.FieldWriter(fieldInfo) { @Override public byte[] copyValue(byte[] value) { return ArrayUtil.copyOfSubArray(value, 0, dim); } }; - case FLOAT32 -> new Lucene99FlatVectorsWriter.FieldWriter<>( - fieldInfo, (KnnFieldVectorsWriter) indexWriter) { + case FLOAT32 -> new Lucene99FlatVectorsWriter.FieldWriter(fieldInfo) { @Override public float[] copyValue(float[] value) { return ArrayUtil.copyOfSubArray(value, 0, dim); @@ -427,8 +419,8 @@ public final class Lucene99FlatVectorsWriter extends FlatVectorsWriter { }; } - FieldWriter(FieldInfo fieldInfo, KnnFieldVectorsWriter indexWriter) { - super(indexWriter); + FieldWriter(FieldInfo fieldInfo) { + super(); this.fieldInfo = fieldInfo; this.dim = fieldInfo.getVectorDimension(); this.docsWithField = new DocsWithFieldSet(); @@ -437,6 +429,9 @@ public final class Lucene99FlatVectorsWriter extends FlatVectorsWriter { @Override public void addValue(int docID, T vectorValue) throws IOException { + if (finished) { + throw new IllegalStateException("already finished, cannot add more values"); + } if (docID == lastDocID) { throw new IllegalArgumentException( "VectorValuesField \"" @@ -448,17 +443,11 @@ public final class Lucene99FlatVectorsWriter extends FlatVectorsWriter { docsWithField.add(docID); vectors.add(copy); lastDocID = docID; - if (indexingDelegate != null) { - indexingDelegate.addValue(docID, copy); - } } @Override public long ramBytesUsed() { long size = SHALLOW_RAM_BYTES_USED; - if (indexingDelegate != null) { - size += indexingDelegate.ramBytesUsed(); - } if (vectors.size() == 0) return size; return size + docsWithField.ramBytesUsed() @@ -468,6 +457,29 @@ public final class Lucene99FlatVectorsWriter extends FlatVectorsWriter { * fieldInfo.getVectorDimension() * fieldInfo.getVectorEncoding().byteSize; } + + @Override + public List getVectors() { + return vectors; + } + + @Override + public DocsWithFieldSet getDocsWithFieldSet() { + return docsWithField; + } + + @Override + public void finish() throws IOException { + if (finished) { + return; + } + this.finished = true; + } + + @Override + public boolean isFinished() { + return finished; + } } static final class FlatCloseableRandomVectorScorerSupplier diff --git a/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99HnswVectorsWriter.java b/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99HnswVectorsWriter.java index 949507848bf..bf97426738b 100644 --- a/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99HnswVectorsWriter.java +++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99HnswVectorsWriter.java @@ -24,9 +24,11 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Objects; import org.apache.lucene.codecs.CodecUtil; import org.apache.lucene.codecs.KnnFieldVectorsWriter; import org.apache.lucene.codecs.KnnVectorsWriter; +import org.apache.lucene.codecs.hnsw.FlatFieldVectorsWriter; import org.apache.lucene.codecs.hnsw.FlatVectorsScorer; import org.apache.lucene.codecs.hnsw.FlatVectorsWriter; import org.apache.lucene.index.DocsWithFieldSet; @@ -130,12 +132,13 @@ public final class Lucene99HnswVectorsWriter extends KnnVectorsWriter { FieldWriter newField = FieldWriter.create( flatVectorWriter.getFlatVectorScorer(), + flatVectorWriter.addField(fieldInfo), fieldInfo, M, beamWidth, segmentWriteState.infoStream); fields.add(newField); - return flatVectorWriter.addField(fieldInfo, newField); + return newField; } @Override @@ -171,8 +174,10 @@ public final class Lucene99HnswVectorsWriter extends KnnVectorsWriter { @Override public long ramBytesUsed() { long total = SHALLOW_RAM_BYTES_USED; - // The vector delegate will also account for this writer's KnnFieldVectorsWriter objects - total += flatVectorWriter.ramBytesUsed(); + for (FieldWriter field : fields) { + // the field tracks the delegate field usage + total += field.ramBytesUsed(); + } return total; } @@ -187,17 +192,19 @@ public final class Lucene99HnswVectorsWriter extends KnnVectorsWriter { fieldData.fieldInfo, vectorIndexOffset, vectorIndexLength, - fieldData.docsWithField.cardinality(), + fieldData.getDocsWithFieldSet().cardinality(), graph, graphLevelNodeOffsets); } private void writeSortingField(FieldWriter fieldData, Sorter.DocMap sortMap) throws IOException { - final int[] ordMap = new int[fieldData.docsWithField.cardinality()]; // new ord to old ord - final int[] oldOrdMap = new int[fieldData.docsWithField.cardinality()]; // old ord to new ord + final int[] ordMap = + new int[fieldData.getDocsWithFieldSet().cardinality()]; // new ord to old ord + final int[] oldOrdMap = + new int[fieldData.getDocsWithFieldSet().cardinality()]; // old ord to new ord - mapOldOrdToNewOrd(fieldData.docsWithField, sortMap, oldOrdMap, ordMap, null); + mapOldOrdToNewOrd(fieldData.getDocsWithFieldSet(), sortMap, oldOrdMap, ordMap, null); // write graph long vectorIndexOffset = vectorIndex.getFilePointer(); OnHeapHnswGraph graph = fieldData.getGraph(); @@ -209,7 +216,7 @@ public final class Lucene99HnswVectorsWriter extends KnnVectorsWriter { fieldData.fieldInfo, vectorIndexOffset, vectorIndexLength, - fieldData.docsWithField.cardinality(), + fieldData.getDocsWithFieldSet().cardinality(), mockGraph, graphLevelNodeOffsets); } @@ -521,42 +528,65 @@ public final class Lucene99HnswVectorsWriter extends KnnVectorsWriter { RamUsageEstimator.shallowSizeOfInstance(FieldWriter.class); private final FieldInfo fieldInfo; - private final DocsWithFieldSet docsWithField; - private final List vectors; private final HnswGraphBuilder hnswGraphBuilder; private int lastDocID = -1; private int node = 0; + private final FlatFieldVectorsWriter flatFieldVectorsWriter; + @SuppressWarnings("unchecked") static FieldWriter create( - FlatVectorsScorer scorer, FieldInfo fieldInfo, int M, int beamWidth, InfoStream infoStream) + FlatVectorsScorer scorer, + FlatFieldVectorsWriter flatFieldVectorsWriter, + FieldInfo fieldInfo, + int M, + int beamWidth, + InfoStream infoStream) throws IOException { return switch (fieldInfo.getVectorEncoding()) { - case BYTE -> new FieldWriter(scorer, fieldInfo, M, beamWidth, infoStream); - case FLOAT32 -> new FieldWriter(scorer, fieldInfo, M, beamWidth, infoStream); + case BYTE -> new FieldWriter<>( + scorer, + (FlatFieldVectorsWriter) flatFieldVectorsWriter, + fieldInfo, + M, + beamWidth, + infoStream); + case FLOAT32 -> new FieldWriter<>( + scorer, + (FlatFieldVectorsWriter) flatFieldVectorsWriter, + fieldInfo, + M, + beamWidth, + infoStream); }; } @SuppressWarnings("unchecked") FieldWriter( - FlatVectorsScorer scorer, FieldInfo fieldInfo, int M, int beamWidth, InfoStream infoStream) + FlatVectorsScorer scorer, + FlatFieldVectorsWriter flatFieldVectorsWriter, + FieldInfo fieldInfo, + int M, + int beamWidth, + InfoStream infoStream) throws IOException { this.fieldInfo = fieldInfo; - this.docsWithField = new DocsWithFieldSet(); - vectors = new ArrayList<>(); RandomVectorScorerSupplier scorerSupplier = switch (fieldInfo.getVectorEncoding()) { case BYTE -> scorer.getRandomVectorScorerSupplier( fieldInfo.getVectorSimilarityFunction(), RandomAccessVectorValues.fromBytes( - (List) vectors, fieldInfo.getVectorDimension())); + (List) flatFieldVectorsWriter.getVectors(), + fieldInfo.getVectorDimension())); case FLOAT32 -> scorer.getRandomVectorScorerSupplier( fieldInfo.getVectorSimilarityFunction(), RandomAccessVectorValues.fromFloats( - (List) vectors, fieldInfo.getVectorDimension())); + (List) flatFieldVectorsWriter.getVectors(), + fieldInfo.getVectorDimension())); }; hnswGraphBuilder = HnswGraphBuilder.create(scorerSupplier, M, beamWidth, HnswGraphBuilder.randSeed); hnswGraphBuilder.setInfoStream(infoStream); + this.flatFieldVectorsWriter = Objects.requireNonNull(flatFieldVectorsWriter); } @Override @@ -567,20 +597,23 @@ public final class Lucene99HnswVectorsWriter extends KnnVectorsWriter { + fieldInfo.name + "\" appears more than once in this document (only one value is allowed per field)"); } - assert docID > lastDocID; - vectors.add(vectorValue); - docsWithField.add(docID); + flatFieldVectorsWriter.addValue(docID, vectorValue); hnswGraphBuilder.addGraphNode(node); node++; lastDocID = docID; } + public DocsWithFieldSet getDocsWithFieldSet() { + return flatFieldVectorsWriter.getDocsWithFieldSet(); + } + @Override public T copyValue(T vectorValue) { throw new UnsupportedOperationException(); } OnHeapHnswGraph getGraph() { + assert flatFieldVectorsWriter.isFinished(); if (node > 0) { return hnswGraphBuilder.getGraph(); } else { @@ -591,9 +624,7 @@ public final class Lucene99HnswVectorsWriter extends KnnVectorsWriter { @Override public long ramBytesUsed() { return SHALLOW_SIZE - + docsWithField.ramBytesUsed() - + (long) vectors.size() - * (RamUsageEstimator.NUM_BYTES_OBJECT_REF + RamUsageEstimator.NUM_BYTES_ARRAY_HEADER) + + flatFieldVectorsWriter.ramBytesUsed() + hnswGraphBuilder.getGraph().ramBytesUsed(); } } diff --git a/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99ScalarQuantizedVectorsWriter.java b/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99ScalarQuantizedVectorsWriter.java index beb1af19ca1..311f2df435e 100644 --- a/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99ScalarQuantizedVectorsWriter.java +++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene99/Lucene99ScalarQuantizedVectorsWriter.java @@ -30,8 +30,8 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import org.apache.lucene.codecs.CodecUtil; -import org.apache.lucene.codecs.KnnFieldVectorsWriter; import org.apache.lucene.codecs.KnnVectorsReader; import org.apache.lucene.codecs.KnnVectorsWriter; import org.apache.lucene.codecs.hnsw.FlatFieldVectorsWriter; @@ -56,7 +56,6 @@ import org.apache.lucene.store.IndexInput; import org.apache.lucene.store.IndexOutput; import org.apache.lucene.util.IOUtils; import org.apache.lucene.util.InfoStream; -import org.apache.lucene.util.RamUsageEstimator; import org.apache.lucene.util.VectorUtil; import org.apache.lucene.util.hnsw.CloseableRandomVectorScorerSupplier; import org.apache.lucene.util.hnsw.RandomVectorScorer; @@ -195,8 +194,8 @@ public final class Lucene99ScalarQuantizedVectorsWriter extends FlatVectorsWrite } @Override - public FlatFieldVectorsWriter addField( - FieldInfo fieldInfo, KnnFieldVectorsWriter indexWriter) throws IOException { + public FlatFieldVectorsWriter addField(FieldInfo fieldInfo) throws IOException { + FlatFieldVectorsWriter rawVectorDelegate = this.rawVectorDelegate.addField(fieldInfo); if (fieldInfo.getVectorEncoding().equals(VectorEncoding.FLOAT32)) { if (bits <= 4 && fieldInfo.getVectorDimension() % 2 != 0) { throw new IllegalArgumentException( @@ -205,6 +204,7 @@ public final class Lucene99ScalarQuantizedVectorsWriter extends FlatVectorsWrite + " is not supported for odd vector dimensions; vector dimension=" + fieldInfo.getVectorDimension()); } + @SuppressWarnings("unchecked") FieldWriter quantizedWriter = new FieldWriter( confidenceInterval, @@ -212,11 +212,11 @@ public final class Lucene99ScalarQuantizedVectorsWriter extends FlatVectorsWrite compress, fieldInfo, segmentWriteState.infoStream, - indexWriter); + (FlatFieldVectorsWriter) rawVectorDelegate); fields.add(quantizedWriter); - indexWriter = quantizedWriter; + return quantizedWriter; } - return rawVectorDelegate.addField(fieldInfo, indexWriter); + return rawVectorDelegate; } @Override @@ -270,12 +270,13 @@ public final class Lucene99ScalarQuantizedVectorsWriter extends FlatVectorsWrite public void flush(int maxDoc, Sorter.DocMap sortMap) throws IOException { rawVectorDelegate.flush(maxDoc, sortMap); for (FieldWriter field : fields) { - field.finish(); + ScalarQuantizer quantizer = field.createQuantizer(); if (sortMap == null) { - writeField(field, maxDoc); + writeField(field, maxDoc, quantizer); } else { - writeSortingField(field, maxDoc, sortMap); + writeSortingField(field, maxDoc, sortMap, quantizer); } + field.finish(); } } @@ -299,15 +300,18 @@ public final class Lucene99ScalarQuantizedVectorsWriter extends FlatVectorsWrite @Override public long ramBytesUsed() { long total = SHALLOW_RAM_BYTES_USED; - // The vector delegate will also account for this writer's KnnFieldVectorsWriter objects - total += rawVectorDelegate.ramBytesUsed(); + for (FieldWriter field : fields) { + // the field tracks the delegate field usage + total += field.ramBytesUsed(); + } return total; } - private void writeField(FieldWriter fieldData, int maxDoc) throws IOException { + private void writeField(FieldWriter fieldData, int maxDoc, ScalarQuantizer scalarQuantizer) + throws IOException { // write vector values long vectorDataOffset = quantizedVectorData.alignFilePointer(Float.BYTES); - writeQuantizedVectors(fieldData); + writeQuantizedVectors(fieldData, scalarQuantizer); long vectorDataLength = quantizedVectorData.getFilePointer() - vectorDataOffset; writeMeta( @@ -318,9 +322,9 @@ public final class Lucene99ScalarQuantizedVectorsWriter extends FlatVectorsWrite confidenceInterval, bits, compress, - fieldData.minQuantile, - fieldData.maxQuantile, - fieldData.docsWithField); + scalarQuantizer.getLowerQuantile(), + scalarQuantizer.getUpperQuantile(), + fieldData.getDocsWithFieldSet()); } private void writeMeta( @@ -365,8 +369,8 @@ public final class Lucene99ScalarQuantizedVectorsWriter extends FlatVectorsWrite DIRECT_MONOTONIC_BLOCK_SHIFT, meta, quantizedVectorData, count, maxDoc, docsWithField); } - private void writeQuantizedVectors(FieldWriter fieldData) throws IOException { - ScalarQuantizer scalarQuantizer = fieldData.createQuantizer(); + private void writeQuantizedVectors(FieldWriter fieldData, ScalarQuantizer scalarQuantizer) + throws IOException { byte[] vector = new byte[fieldData.fieldInfo.getVectorDimension()]; byte[] compressedVector = fieldData.compress @@ -375,7 +379,8 @@ public final class Lucene99ScalarQuantizedVectorsWriter extends FlatVectorsWrite : null; final ByteBuffer offsetBuffer = ByteBuffer.allocate(Float.BYTES).order(ByteOrder.LITTLE_ENDIAN); float[] copy = fieldData.normalize ? new float[fieldData.fieldInfo.getVectorDimension()] : null; - for (float[] v : fieldData.floatVectors) { + assert fieldData.getVectors().isEmpty() || scalarQuantizer != null; + for (float[] v : fieldData.getVectors()) { if (fieldData.normalize) { System.arraycopy(v, 0, copy, 0, copy.length); VectorUtil.l2normalize(copy); @@ -396,16 +401,18 @@ public final class Lucene99ScalarQuantizedVectorsWriter extends FlatVectorsWrite } } - private void writeSortingField(FieldWriter fieldData, int maxDoc, Sorter.DocMap sortMap) + private void writeSortingField( + FieldWriter fieldData, int maxDoc, Sorter.DocMap sortMap, ScalarQuantizer scalarQuantizer) throws IOException { - final int[] ordMap = new int[fieldData.docsWithField.cardinality()]; // new ord to old ord + final int[] ordMap = + new int[fieldData.getDocsWithFieldSet().cardinality()]; // new ord to old ord DocsWithFieldSet newDocsWithField = new DocsWithFieldSet(); - mapOldOrdToNewOrd(fieldData.docsWithField, sortMap, null, ordMap, newDocsWithField); + mapOldOrdToNewOrd(fieldData.getDocsWithFieldSet(), sortMap, null, ordMap, newDocsWithField); // write vector values long vectorDataOffset = quantizedVectorData.alignFilePointer(Float.BYTES); - writeSortedQuantizedVectors(fieldData, ordMap); + writeSortedQuantizedVectors(fieldData, ordMap, scalarQuantizer); long quantizedVectorLength = quantizedVectorData.getFilePointer() - vectorDataOffset; writeMeta( fieldData.fieldInfo, @@ -415,13 +422,13 @@ public final class Lucene99ScalarQuantizedVectorsWriter extends FlatVectorsWrite confidenceInterval, bits, compress, - fieldData.minQuantile, - fieldData.maxQuantile, + scalarQuantizer.getLowerQuantile(), + scalarQuantizer.getUpperQuantile(), newDocsWithField); } - private void writeSortedQuantizedVectors(FieldWriter fieldData, int[] ordMap) throws IOException { - ScalarQuantizer scalarQuantizer = fieldData.createQuantizer(); + private void writeSortedQuantizedVectors( + FieldWriter fieldData, int[] ordMap, ScalarQuantizer scalarQuantizer) throws IOException { byte[] vector = new byte[fieldData.fieldInfo.getVectorDimension()]; byte[] compressedVector = fieldData.compress @@ -431,7 +438,7 @@ public final class Lucene99ScalarQuantizedVectorsWriter extends FlatVectorsWrite final ByteBuffer offsetBuffer = ByteBuffer.allocate(Float.BYTES).order(ByteOrder.LITTLE_ENDIAN); float[] copy = fieldData.normalize ? new float[fieldData.fieldInfo.getVectorDimension()] : null; for (int ordinal : ordMap) { - float[] v = fieldData.floatVectors.get(ordinal); + float[] v = fieldData.getVectors().get(ordinal); if (fieldData.normalize) { System.arraycopy(v, 0, copy, 0, copy.length); VectorUtil.l2normalize(copy); @@ -744,44 +751,51 @@ public final class Lucene99ScalarQuantizedVectorsWriter extends FlatVectorsWrite static class FieldWriter extends FlatFieldVectorsWriter { private static final long SHALLOW_SIZE = shallowSizeOfInstance(FieldWriter.class); - private final List floatVectors; private final FieldInfo fieldInfo; private final Float confidenceInterval; private final byte bits; private final boolean compress; private final InfoStream infoStream; private final boolean normalize; - private float minQuantile = Float.POSITIVE_INFINITY; - private float maxQuantile = Float.NEGATIVE_INFINITY; private boolean finished; - private final DocsWithFieldSet docsWithField; + private final FlatFieldVectorsWriter flatFieldVectorsWriter; - @SuppressWarnings("unchecked") FieldWriter( Float confidenceInterval, byte bits, boolean compress, FieldInfo fieldInfo, InfoStream infoStream, - KnnFieldVectorsWriter indexWriter) { - super((KnnFieldVectorsWriter) indexWriter); + FlatFieldVectorsWriter indexWriter) { + super(); this.confidenceInterval = confidenceInterval; this.bits = bits; this.fieldInfo = fieldInfo; this.normalize = fieldInfo.getVectorSimilarityFunction() == VectorSimilarityFunction.COSINE; - this.floatVectors = new ArrayList<>(); this.infoStream = infoStream; - this.docsWithField = new DocsWithFieldSet(); this.compress = compress; + this.flatFieldVectorsWriter = Objects.requireNonNull(indexWriter); } - void finish() throws IOException { + @Override + public boolean isFinished() { + return finished && flatFieldVectorsWriter.isFinished(); + } + + @Override + public void finish() throws IOException { if (finished) { return; } + assert flatFieldVectorsWriter.isFinished(); + finished = true; + } + + ScalarQuantizer createQuantizer() throws IOException { + assert flatFieldVectorsWriter.isFinished(); + List floatVectors = flatFieldVectorsWriter.getVectors(); if (floatVectors.size() == 0) { - finished = true; - return; + return new ScalarQuantizer(0, 0, bits); } FloatVectorValues floatVectorValues = new FloatVectorWrapper(floatVectors, normalize); ScalarQuantizer quantizer = @@ -791,8 +805,6 @@ public final class Lucene99ScalarQuantizedVectorsWriter extends FlatVectorsWrite fieldInfo.getVectorSimilarityFunction(), confidenceInterval, bits); - minQuantile = quantizer.getLowerQuantile(); - maxQuantile = quantizer.getUpperQuantile(); if (infoStream.isEnabled(QUANTIZED_VECTOR_COMPONENT)) { infoStream.message( QUANTIZED_VECTOR_COMPONENT, @@ -802,41 +814,39 @@ public final class Lucene99ScalarQuantizedVectorsWriter extends FlatVectorsWrite + " bits=" + bits + " minQuantile=" - + minQuantile + + quantizer.getLowerQuantile() + " maxQuantile=" - + maxQuantile); + + quantizer.getUpperQuantile()); } - finished = true; - } - - ScalarQuantizer createQuantizer() { - assert finished; - return new ScalarQuantizer(minQuantile, maxQuantile, bits); + return quantizer; } @Override public long ramBytesUsed() { long size = SHALLOW_SIZE; - if (indexingDelegate != null) { - size += indexingDelegate.ramBytesUsed(); - } - if (floatVectors.size() == 0) return size; - return size + (long) floatVectors.size() * RamUsageEstimator.NUM_BYTES_OBJECT_REF; + size += flatFieldVectorsWriter.ramBytesUsed(); + return size; } @Override public void addValue(int docID, float[] vectorValue) throws IOException { - docsWithField.add(docID); - floatVectors.add(vectorValue); - if (indexingDelegate != null) { - indexingDelegate.addValue(docID, vectorValue); - } + flatFieldVectorsWriter.addValue(docID, vectorValue); } @Override public float[] copyValue(float[] vectorValue) { throw new UnsupportedOperationException(); } + + @Override + public List getVectors() { + return flatFieldVectorsWriter.getVectors(); + } + + @Override + public DocsWithFieldSet getDocsWithFieldSet() { + return flatFieldVectorsWriter.getDocsWithFieldSet(); + } } static class FloatVectorWrapper extends FloatVectorValues { From 49e781084a5bbf6d5fca0317d8a623e8f58cc2b5 Mon Sep 17 00:00:00 2001 From: Jakub Slowinski <32519034+slow-J@users.noreply.github.com> Date: Wed, 10 Jul 2024 17:05:09 +0100 Subject: [PATCH 42/42] Minor cleanup in some Facet tests (#13489) --- .../apache/lucene/facet/FacetTestCase.java | 47 +++++------ .../facet/TestLongValueFacetCounts.java | 44 ++++------ .../lucene/facet/TestMultipleIndexFields.java | 36 ++++---- .../TestRandomSamplingFacetsCollector.java | 2 +- .../facet/TestStringValueFacetCounts.java | 9 +- .../facet/range/TestRangeFacetCounts.java | 40 ++------- .../TestRangeOnRangeFacetCounts.java | 74 ++++------------- .../lucene/facet/taxonomy/TestFacetLabel.java | 83 ++++--------------- .../taxonomy/TestSearcherTaxonomyManager.java | 6 +- .../facet/taxonomy/TestTaxonomyCombined.java | 19 +---- .../TestTaxonomyFacetAssociations.java | 48 +++-------- .../taxonomy/TestTaxonomyFacetCounts2.java | 24 +++--- .../TestTaxonomyFacetValueSource.java | 42 ++-------- .../taxonomy/directory/TestAddTaxonomy.java | 56 +++++-------- .../TestConcurrentFacetedIndexing.java | 47 +++++------ .../TestDirectoryTaxonomyReader.java | 78 +++++++++-------- .../TestDirectoryTaxonomyWriter.java | 58 +++++++------ 17 files changed, 237 insertions(+), 476 deletions(-) diff --git a/lucene/facet/src/test/org/apache/lucene/facet/FacetTestCase.java b/lucene/facet/src/test/org/apache/lucene/facet/FacetTestCase.java index 7df130344f0..1023bb4cce3 100644 --- a/lucene/facet/src/test/org/apache/lucene/facet/FacetTestCase.java +++ b/lucene/facet/src/test/org/apache/lucene/facet/FacetTestCase.java @@ -20,7 +20,6 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -85,7 +84,8 @@ public abstract class FacetTestCase extends LuceneTestCase { * @param docId docId for which facet labels are needed. * @param dimension Retain facet labels for supplied dimension only. A null value fetches all * facet labels. - * @param facetLabelReader {@FacetLabelReader} instance use to get facet labels for input docId. + * @param facetLabelReader {@link FacetLabelReader} instance use to get facet labels for input + * docId. * @return {@code List} containing matching facet labels. * @throws IOException when a low-level IO issue occurs while reading facet labels. */ @@ -178,12 +178,9 @@ public abstract class FacetTestCase extends LuceneTestCase { labelValues, i - numInRow, i, - new Comparator() { - @Override - public int compare(LabelAndValue a, LabelAndValue b) { - assert a.value.doubleValue() == b.value.doubleValue(); - return new BytesRef(a.label).compareTo(new BytesRef(b.label)); - } + (a, b) -> { + assert a.value.doubleValue() == b.value.doubleValue(); + return new BytesRef(a.label).compareTo(new BytesRef(b.label)); }); } numInRow = 1; @@ -198,16 +195,13 @@ public abstract class FacetTestCase extends LuceneTestCase { protected void sortLabelValues(List labelValues) { Collections.sort( labelValues, - new Comparator() { - @Override - public int compare(LabelAndValue a, LabelAndValue b) { - if (a.value.doubleValue() > b.value.doubleValue()) { - return -1; - } else if (a.value.doubleValue() < b.value.doubleValue()) { - return 1; - } else { - return new BytesRef(a.label).compareTo(new BytesRef(b.label)); - } + (a, b) -> { + if (a.value.doubleValue() > b.value.doubleValue()) { + return -1; + } else if (a.value.doubleValue() < b.value.doubleValue()) { + return 1; + } else { + return new BytesRef(a.label).compareTo(new BytesRef(b.label)); } }); } @@ -215,16 +209,13 @@ public abstract class FacetTestCase extends LuceneTestCase { protected void sortFacetResults(List results) { Collections.sort( results, - new Comparator() { - @Override - public int compare(FacetResult a, FacetResult b) { - if (a.value.doubleValue() > b.value.doubleValue()) { - return -1; - } else if (b.value.doubleValue() > a.value.doubleValue()) { - return 1; - } else { - return a.dim.compareTo(b.dim); - } + (a, b) -> { + if (a.value.doubleValue() > b.value.doubleValue()) { + return -1; + } else if (b.value.doubleValue() > a.value.doubleValue()) { + return 1; + } else { + return a.dim.compareTo(b.dim); } }); } diff --git a/lucene/facet/src/test/org/apache/lucene/facet/TestLongValueFacetCounts.java b/lucene/facet/src/test/org/apache/lucene/facet/TestLongValueFacetCounts.java index a35b76ff1d5..a049b96012c 100644 --- a/lucene/facet/src/test/org/apache/lucene/facet/TestLongValueFacetCounts.java +++ b/lucene/facet/src/test/org/apache/lucene/facet/TestLongValueFacetCounts.java @@ -80,14 +80,12 @@ public class TestLongValueFacetCounts extends FacetTestCase { new String[0], 6, 101, - new LabelAndValue[] { - new LabelAndValue("0", 20), - new LabelAndValue("1", 20), - new LabelAndValue("2", 20), - new LabelAndValue("3", 20), - new LabelAndValue("4", 20), - new LabelAndValue("9223372036854775807", 1) - }); + new LabelAndValue("0", 20), + new LabelAndValue("1", 20), + new LabelAndValue("2", 20), + new LabelAndValue("3", 20), + new LabelAndValue("4", 20), + new LabelAndValue("9223372036854775807", 1)); r.close(); d.close(); @@ -123,9 +121,8 @@ public class TestLongValueFacetCounts extends FacetTestCase { new String[0], 2, 9, - new LabelAndValue[] { - new LabelAndValue("0", 4), new LabelAndValue("1", 5), - }); + new LabelAndValue("0", 4), + new LabelAndValue("1", 5)); r.close(); d.close(); @@ -156,11 +153,9 @@ public class TestLongValueFacetCounts extends FacetTestCase { new String[0], 3, 3, - new LabelAndValue[] { - new LabelAndValue("9223372036854775805", 1), - new LabelAndValue("9223372036854775806", 1), - new LabelAndValue("9223372036854775807", 1) - }); + new LabelAndValue("9223372036854775805", 1), + new LabelAndValue("9223372036854775806", 1), + new LabelAndValue("9223372036854775807", 1)); // since we have no insight into the value order in the hashMap, we sort labels by value and // count in @@ -221,11 +216,7 @@ public class TestLongValueFacetCounts extends FacetTestCase { List topDimsResults2 = facets.getTopDims(0, 1); assertEquals(0, topDimsResults2.size()); // test getAllDims(0) - expectThrows( - IllegalArgumentException.class, - () -> { - facets.getAllDims(0); - }); + expectThrows(IllegalArgumentException.class, () -> facets.getAllDims(0)); r.close(); d.close(); @@ -364,8 +355,7 @@ public class TestLongValueFacetCounts extends FacetTestCase { // test getAllChildren expectedCounts.sort( - Comparator.comparing((Map.Entry a) -> a.getKey()) - .thenComparingLong(Map.Entry::getValue)); + Map.Entry.comparingByKey().thenComparingLong(Map.Entry::getValue)); FacetResult allChildren = facetCounts.getAllChildren("field"); // sort labels by value, count in ascending order Arrays.sort( @@ -627,8 +617,7 @@ public class TestLongValueFacetCounts extends FacetTestCase { // test getAllChildren expectedCounts.sort( - Comparator.comparing((Map.Entry a) -> a.getKey()) - .thenComparingLong(Map.Entry::getValue)); + Map.Entry.comparingByKey().thenComparingLong(Map.Entry::getValue)); FacetResult allChildren = facetCounts.getAllChildren("field"); // sort labels by value, count in ascending order Arrays.sort( @@ -833,9 +822,8 @@ public class TestLongValueFacetCounts extends FacetTestCase { new String[0], 2, 2, - new LabelAndValue[] { - new LabelAndValue("42", 1), new LabelAndValue("43", 1), - }); + new LabelAndValue("42", 1), + new LabelAndValue("43", 1)); r.close(); dir.close(); diff --git a/lucene/facet/src/test/org/apache/lucene/facet/TestMultipleIndexFields.java b/lucene/facet/src/test/org/apache/lucene/facet/TestMultipleIndexFields.java index d89a1fb38d0..706c09a9837 100644 --- a/lucene/facet/src/test/org/apache/lucene/facet/TestMultipleIndexFields.java +++ b/lucene/facet/src/test/org/apache/lucene/facet/TestMultipleIndexFields.java @@ -86,7 +86,7 @@ public class TestMultipleIndexFields extends FacetTestCase { // prepare searcher to search against IndexSearcher searcher = newSearcher(ir); - FacetsCollector sfc = performSearch(tr, ir, searcher); + FacetsCollector sfc = performSearch(searcher); // Obtain facets results and hand-test them assertCorrectResults(getTaxonomyFacetCounts(tr, config, sfc)); @@ -124,7 +124,7 @@ public class TestMultipleIndexFields extends FacetTestCase { // prepare searcher to search against IndexSearcher searcher = newSearcher(ir); - FacetsCollector sfc = performSearch(tr, ir, searcher); + FacetsCollector sfc = performSearch(searcher); Map facetsMap = new HashMap<>(); facetsMap.put("Author", getTaxonomyFacetCounts(tr, config, sfc, "$author")); @@ -168,7 +168,7 @@ public class TestMultipleIndexFields extends FacetTestCase { // prepare searcher to search against IndexSearcher searcher = newSearcher(ir); - FacetsCollector sfc = performSearch(tr, ir, searcher); + FacetsCollector sfc = performSearch(searcher); Map facetsMap = new HashMap<>(); Facets facets2 = getTaxonomyFacetCounts(tr, config, sfc, "$music"); @@ -225,7 +225,7 @@ public class TestMultipleIndexFields extends FacetTestCase { // prepare searcher to search against IndexSearcher searcher = newSearcher(ir); - FacetsCollector sfc = performSearch(tr, ir, searcher); + FacetsCollector sfc = performSearch(searcher); Map facetsMap = new HashMap<>(); facetsMap.put("Band", getTaxonomyFacetCounts(tr, config, sfc, "$bands")); @@ -271,7 +271,7 @@ public class TestMultipleIndexFields extends FacetTestCase { // prepare searcher to search against IndexSearcher searcher = newSearcher(ir); - FacetsCollector sfc = performSearch(tr, ir, searcher); + FacetsCollector sfc = performSearch(searcher); Map facetsMap = new HashMap<>(); Facets facets2 = getTaxonomyFacetCounts(tr, config, sfc, "$music"); @@ -300,9 +300,8 @@ public class TestMultipleIndexFields extends FacetTestCase { new String[0], 2, 5, - new LabelAndValue[] { - new LabelAndValue("Punk", 1), new LabelAndValue("Rock & Pop", 4), - }); + new LabelAndValue("Punk", 1), + new LabelAndValue("Rock & Pop", 4)); assertEquals( "dim=Band path=[Rock & Pop] value=4 childCount=4\n The Beatles (1)\n U2 (1)\n REM (1)\n Dave Matthews Band (1)\n", facets.getTopChildren(10, "Band", "Rock & Pop").toString()); @@ -312,12 +311,10 @@ public class TestMultipleIndexFields extends FacetTestCase { new String[] {"Rock & Pop"}, 4, 4, - new LabelAndValue[] { - new LabelAndValue("Dave Matthews Band", 1), - new LabelAndValue("REM", 1), - new LabelAndValue("The Beatles", 1), - new LabelAndValue("U2", 1), - }); + new LabelAndValue("Dave Matthews Band", 1), + new LabelAndValue("REM", 1), + new LabelAndValue("The Beatles", 1), + new LabelAndValue("U2", 1)); assertEquals( "dim=Author path=[] value=3 childCount=3\n Mark Twain (1)\n Stephen King (1)\n Kurt Vonnegut (1)\n", @@ -328,15 +325,12 @@ public class TestMultipleIndexFields extends FacetTestCase { new String[0], 3, 3, - new LabelAndValue[] { - new LabelAndValue("Kurt Vonnegut", 1), - new LabelAndValue("Mark Twain", 1), - new LabelAndValue("Stephen King", 1), - }); + new LabelAndValue("Kurt Vonnegut", 1), + new LabelAndValue("Mark Twain", 1), + new LabelAndValue("Stephen King", 1)); } - private FacetsCollector performSearch(TaxonomyReader tr, IndexReader ir, IndexSearcher searcher) - throws IOException { + private FacetsCollector performSearch(IndexSearcher searcher) throws IOException { FacetsCollector fc = new FacetsCollector(); FacetsCollector.search(searcher, new MatchAllDocsQuery(), 10, fc); return fc; diff --git a/lucene/facet/src/test/org/apache/lucene/facet/TestRandomSamplingFacetsCollector.java b/lucene/facet/src/test/org/apache/lucene/facet/TestRandomSamplingFacetsCollector.java index df51e0afc38..9eb45ba2c3a 100644 --- a/lucene/facet/src/test/org/apache/lucene/facet/TestRandomSamplingFacetsCollector.java +++ b/lucene/facet/src/test/org/apache/lucene/facet/TestRandomSamplingFacetsCollector.java @@ -119,7 +119,7 @@ public class TestRandomSamplingFacetsCollector extends FacetTestCase { float ei = (float) md.totalHits / totalHits; if (ei > 0.0f) { float oi = (float) numSampledDocs[i] / totalSampledDocs; - chi_square += (Math.pow(ei - oi, 2) / ei); + chi_square += (float) (Math.pow(ei - oi, 2) / ei); } } diff --git a/lucene/facet/src/test/org/apache/lucene/facet/TestStringValueFacetCounts.java b/lucene/facet/src/test/org/apache/lucene/facet/TestStringValueFacetCounts.java index 5c9a1eea7b4..9d5a8e238f5 100644 --- a/lucene/facet/src/test/org/apache/lucene/facet/TestStringValueFacetCounts.java +++ b/lucene/facet/src/test/org/apache/lucene/facet/TestStringValueFacetCounts.java @@ -468,11 +468,7 @@ public class TestStringValueFacetCounts extends FacetTestCase { assertEquals(facetResult, topNDimsResult.get(0)); // test getAllDims(0) - expectThrows( - IllegalArgumentException.class, - () -> { - facets.getAllDims(0); - }); + expectThrows(IllegalArgumentException.class, () -> facets.getAllDims(0)); // This is a little strange, but we request all labels at this point so that when we // secondarily sort by label value in order to compare to the expected results, we have @@ -538,8 +534,7 @@ public class TestStringValueFacetCounts extends FacetTestCase { // sort expected counts by value, count expectedCountsSortedByValue.sort( - Comparator.comparing((Map.Entry a) -> a.getKey()) - .thenComparingInt(Map.Entry::getValue)); + Map.Entry.comparingByKey().thenComparingInt(Map.Entry::getValue)); FacetResult facetResult = facets.getAllChildren("field"); assertEquals(expectedTotalDocsWithValue, facetResult.value); diff --git a/lucene/facet/src/test/org/apache/lucene/facet/range/TestRangeFacetCounts.java b/lucene/facet/src/test/org/apache/lucene/facet/range/TestRangeFacetCounts.java index 57d21e4d14d..d7cb507a911 100644 --- a/lucene/facet/src/test/org/apache/lucene/facet/range/TestRangeFacetCounts.java +++ b/lucene/facet/src/test/org/apache/lucene/facet/range/TestRangeFacetCounts.java @@ -112,11 +112,7 @@ public class TestRangeFacetCounts extends FacetTestCase { result.toString()); // test getTopChildren(0, dim) - expectThrows( - IllegalArgumentException.class, - () -> { - facets.getTopChildren(0, "field"); - }); + expectThrows(IllegalArgumentException.class, () -> facets.getTopChildren(0, "field")); r.close(); d.close(); @@ -169,11 +165,7 @@ public class TestRangeFacetCounts extends FacetTestCase { result.toString()); // test getTopChildren(0, dim) - expectThrows( - IllegalArgumentException.class, - () -> { - facets.getTopChildren(0, "field"); - }); + expectThrows(IllegalArgumentException.class, () -> facets.getTopChildren(0, "field")); r.close(); d.close(); @@ -287,37 +279,19 @@ public class TestRangeFacetCounts extends FacetTestCase { assertEquals(0, topNDimsResult.size()); // test getAllDims(0) - expectThrows( - IllegalArgumentException.class, - () -> { - facets.getAllDims(0); - }); + expectThrows(IllegalArgumentException.class, () -> facets.getAllDims(0)); r.close(); d.close(); } public void testUselessRange() { + expectThrows(IllegalArgumentException.class, () -> new LongRange("useless", 7, true, 6, true)); + expectThrows(IllegalArgumentException.class, () -> new LongRange("useless", 7, true, 7, false)); expectThrows( - IllegalArgumentException.class, - () -> { - new LongRange("useless", 7, true, 6, true); - }); + IllegalArgumentException.class, () -> new DoubleRange("useless", 7.0, true, 6.0, true)); expectThrows( - IllegalArgumentException.class, - () -> { - new LongRange("useless", 7, true, 7, false); - }); - expectThrows( - IllegalArgumentException.class, - () -> { - new DoubleRange("useless", 7.0, true, 6.0, true); - }); - expectThrows( - IllegalArgumentException.class, - () -> { - new DoubleRange("useless", 7.0, true, 7.0, false); - }); + IllegalArgumentException.class, () -> new DoubleRange("useless", 7.0, true, 7.0, false)); } public void testLongMinMax() throws Exception { diff --git a/lucene/facet/src/test/org/apache/lucene/facet/rangeonrange/TestRangeOnRangeFacetCounts.java b/lucene/facet/src/test/org/apache/lucene/facet/rangeonrange/TestRangeOnRangeFacetCounts.java index 738b14e71ff..4698e910168 100644 --- a/lucene/facet/src/test/org/apache/lucene/facet/rangeonrange/TestRangeOnRangeFacetCounts.java +++ b/lucene/facet/src/test/org/apache/lucene/facet/rangeonrange/TestRangeOnRangeFacetCounts.java @@ -100,11 +100,7 @@ public class TestRangeOnRangeFacetCounts extends FacetTestCase { result.toString()); // test getTopChildren(0, dim) - expectThrows( - IllegalArgumentException.class, - () -> { - facets.getTopChildren(0, "field"); - }); + expectThrows(IllegalArgumentException.class, () -> facets.getTopChildren(0, "field")); r.close(); d.close(); @@ -160,11 +156,7 @@ public class TestRangeOnRangeFacetCounts extends FacetTestCase { result.toString()); // test getTopChildren(0, dim) - expectThrows( - IllegalArgumentException.class, - () -> { - facets.getTopChildren(0, "field"); - }); + expectThrows(IllegalArgumentException.class, () -> facets.getTopChildren(0, "field")); r.close(); d.close(); @@ -224,11 +216,7 @@ public class TestRangeOnRangeFacetCounts extends FacetTestCase { assertEquals(0, topNDimsResult.size()); // test getAllDims(0) - expectThrows( - IllegalArgumentException.class, - () -> { - facets.getAllDims(0); - }); + expectThrows(IllegalArgumentException.class, () -> facets.getAllDims(0)); r.close(); d.close(); @@ -289,60 +277,34 @@ public class TestRangeOnRangeFacetCounts extends FacetTestCase { result.get(0).toString()); // test getTopChildren(0, dim) - expectThrows( - IllegalArgumentException.class, - () -> { - facets.getTopChildren(0, "field"); - }); + expectThrows(IllegalArgumentException.class, () -> facets.getTopChildren(0, "field")); r.close(); d.close(); } public void testUselessRangeSingleDim() { + expectThrows(IllegalArgumentException.class, () -> new LongRange("useless", 7, true, 6, true)); + expectThrows(IllegalArgumentException.class, () -> new LongRange("useless", 7, true, 7, false)); expectThrows( - IllegalArgumentException.class, - () -> { - new LongRange("useless", 7, true, 6, true); - }); + IllegalArgumentException.class, () -> new DoubleRange("useless", 7.0, true, 6.0, true)); expectThrows( - IllegalArgumentException.class, - () -> { - new LongRange("useless", 7, true, 7, false); - }); - expectThrows( - IllegalArgumentException.class, - () -> { - new DoubleRange("useless", 7.0, true, 6.0, true); - }); - expectThrows( - IllegalArgumentException.class, - () -> { - new DoubleRange("useless", 7.0, true, 7.0, false); - }); + IllegalArgumentException.class, () -> new DoubleRange("useless", 7.0, true, 7.0, false)); } public void testUselessMultiDimRange() { expectThrows( IllegalArgumentException.class, - () -> { - new LongRange("useless", longArray(7L, 7L), longArray(6L, 6L)); - }); + () -> new LongRange("useless", longArray(7L, 7L), longArray(6L, 6L))); expectThrows( IllegalArgumentException.class, - () -> { - new LongRange("useless", longArray(7L, 7L), longArray(7L, 6L)); - }); + () -> new LongRange("useless", longArray(7L, 7L), longArray(7L, 6L))); expectThrows( IllegalArgumentException.class, - () -> { - new DoubleRange("useless", doubleArray(7.0, 7.0), doubleArray(6.0, 6.0)); - }); + () -> new DoubleRange("useless", doubleArray(7.0, 7.0), doubleArray(6.0, 6.0))); expectThrows( IllegalArgumentException.class, - () -> { - new DoubleRange("useless", doubleArray(7.0, 7.0), doubleArray(7.0, 6.0)); - }); + () -> new DoubleRange("useless", doubleArray(7.0, 7.0), doubleArray(7.0, 6.0))); } public void testSingleDimLongMinMax() throws Exception { @@ -769,11 +731,7 @@ public class TestRangeOnRangeFacetCounts extends FacetTestCase { result.toString()); // test getTopChildren(0, dim) - expectThrows( - IllegalArgumentException.class, - () -> { - facets.getTopChildren(0, "field"); - }); + expectThrows(IllegalArgumentException.class, () -> facets.getTopChildren(0, "field")); IOUtils.close(r, d); } @@ -830,11 +788,7 @@ public class TestRangeOnRangeFacetCounts extends FacetTestCase { result.toString()); // test getTopChildren(0, dim) - expectThrows( - IllegalArgumentException.class, - () -> { - facets.getTopChildren(0, "field"); - }); + expectThrows(IllegalArgumentException.class, () -> facets.getTopChildren(0, "field")); IOUtils.close(r, d); } diff --git a/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestFacetLabel.java b/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestFacetLabel.java index ee612dbfd8a..3f24691d252 100644 --- a/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestFacetLabel.java +++ b/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestFacetLabel.java @@ -160,88 +160,43 @@ public class TestFacetLabel extends FacetTestCase { // empty or null components should not be allowed. for (String[] components : components_tests) { + expectThrows(IllegalArgumentException.class, () -> new FacetLabel(components)); + expectThrows(IllegalArgumentException.class, () -> new FacetField("dim", components)); expectThrows( IllegalArgumentException.class, - () -> { - new FacetLabel(components); - }); + () -> new AssociationFacetField(new BytesRef(), "dim", components)); expectThrows( IllegalArgumentException.class, - () -> { - new FacetField("dim", components); - }); + () -> new IntAssociationFacetField(17, "dim", components)); expectThrows( IllegalArgumentException.class, - () -> { - new AssociationFacetField(new BytesRef(), "dim", components); - }); - expectThrows( - IllegalArgumentException.class, - () -> { - new IntAssociationFacetField(17, "dim", components); - }); - expectThrows( - IllegalArgumentException.class, - () -> { - new FloatAssociationFacetField(17.0f, "dim", components); - }); + () -> new FloatAssociationFacetField(17.0f, "dim", components)); } + expectThrows(IllegalArgumentException.class, () -> new FacetField(null, new String[] {"abc"})); + expectThrows(IllegalArgumentException.class, () -> new FacetField("", "abc")); expectThrows( IllegalArgumentException.class, - () -> { - new FacetField(null, new String[] {"abc"}); - }); + () -> new IntAssociationFacetField(17, null, new String[] {"abc"})); expectThrows( IllegalArgumentException.class, - () -> { - new FacetField("", new String[] {"abc"}); - }); + () -> new IntAssociationFacetField(17, "", new String[] {"abc"})); expectThrows( IllegalArgumentException.class, - () -> { - new IntAssociationFacetField(17, null, new String[] {"abc"}); - }); + () -> new FloatAssociationFacetField(17.0f, null, new String[] {"abc"})); expectThrows( IllegalArgumentException.class, - () -> { - new IntAssociationFacetField(17, "", new String[] {"abc"}); - }); + () -> new FloatAssociationFacetField(17.0f, "", new String[] {"abc"})); expectThrows( IllegalArgumentException.class, - () -> { - new FloatAssociationFacetField(17.0f, null, new String[] {"abc"}); - }); + () -> new AssociationFacetField(new BytesRef(), null, "abc")); expectThrows( IllegalArgumentException.class, - () -> { - new FloatAssociationFacetField(17.0f, "", new String[] {"abc"}); - }); + () -> new AssociationFacetField(new BytesRef(), "", new String[] {"abc"})); expectThrows( - IllegalArgumentException.class, - () -> { - new AssociationFacetField(new BytesRef(), null, new String[] {"abc"}); - }); - expectThrows( - IllegalArgumentException.class, - () -> { - new AssociationFacetField(new BytesRef(), "", new String[] {"abc"}); - }); - expectThrows( - IllegalArgumentException.class, - () -> { - new SortedSetDocValuesFacetField(null, "abc"); - }); - expectThrows( - IllegalArgumentException.class, - () -> { - new SortedSetDocValuesFacetField("", "abc"); - }); - expectThrows( - IllegalArgumentException.class, - () -> { - new SortedSetDocValuesFacetField("dim", ""); - }); + IllegalArgumentException.class, () -> new SortedSetDocValuesFacetField(null, "abc")); + expectThrows(IllegalArgumentException.class, () -> new SortedSetDocValuesFacetField("", "abc")); + expectThrows(IllegalArgumentException.class, () -> new SortedSetDocValuesFacetField("dim", "")); } @Test @@ -258,10 +213,6 @@ public class TestFacetLabel extends FacetTestCase { // long paths should not be allowed final String longPath = bigComp; - expectThrows( - IllegalArgumentException.class, - () -> { - new FacetLabel("dim", longPath); - }); + expectThrows(IllegalArgumentException.class, () -> new FacetLabel("dim", longPath)); } } diff --git a/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestSearcherTaxonomyManager.java b/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestSearcherTaxonomyManager.java index d9bf0ce2d7f..10f56e63ad8 100644 --- a/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestSearcherTaxonomyManager.java +++ b/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestSearcherTaxonomyManager.java @@ -287,11 +287,7 @@ public class TestSearcherTaxonomyManager extends FacetTestCase { tw.replaceTaxonomy(taxoDir2); taxoDir2.close(); - expectThrows( - IllegalStateException.class, - () -> { - mgr.maybeRefresh(); - }); + expectThrows(IllegalStateException.class, mgr::maybeRefresh); w.close(); IOUtils.close(mgr, tw, taxoDir, dir); diff --git a/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestTaxonomyCombined.java b/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestTaxonomyCombined.java index fda98d581b2..c04f6f5db97 100644 --- a/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestTaxonomyCombined.java +++ b/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestTaxonomyCombined.java @@ -150,7 +150,7 @@ public class TestTaxonomyCombined extends FacetTestCase { if (path.length == 0) { return ""; } - return "<" + path.toString() + ">"; + return "<" + path + ">"; } /** @@ -525,21 +525,10 @@ public class TestTaxonomyCombined extends FacetTestCase { } // check parent of of invalid ordinals: + expectThrows(IndexOutOfBoundsException.class, () -> tw.getParent(-1)); expectThrows( - IndexOutOfBoundsException.class, - () -> { - tw.getParent(-1); - }); - expectThrows( - IndexOutOfBoundsException.class, - () -> { - tw.getParent(TaxonomyReader.INVALID_ORDINAL); - }); - expectThrows( - IndexOutOfBoundsException.class, - () -> { - tw.getParent(tr.getSize()); - }); + IndexOutOfBoundsException.class, () -> tw.getParent(TaxonomyReader.INVALID_ORDINAL)); + expectThrows(IndexOutOfBoundsException.class, () -> tw.getParent(tr.getSize())); } /** diff --git a/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestTaxonomyFacetAssociations.java b/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestTaxonomyFacetAssociations.java index 48310f80d71..c452a01292e 100644 --- a/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestTaxonomyFacetAssociations.java +++ b/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestTaxonomyFacetAssociations.java @@ -227,9 +227,8 @@ public class TestTaxonomyFacetAssociations extends FacetTestCase { 2, -1, Map.of("a", 100, "b", 50), - new LabelAndValue[] { - new LabelAndValue("a", 200), new LabelAndValue("b", 150), - }); + new LabelAndValue("a", 200), + new LabelAndValue("b", 150)); assertEquals( "Wrong count for category 'a'!", 200, facets.getSpecificValue("int", "a").intValue()); assertEquals( @@ -311,9 +310,8 @@ public class TestTaxonomyFacetAssociations extends FacetTestCase { 2, -1f, Map.of("a", 100, "b", 50), - new LabelAndValue[] { - new LabelAndValue("a", 50.0f), new LabelAndValue("b", 9.999995f), - }); + new LabelAndValue("a", 50.0f), + new LabelAndValue("b", 9.999995f)); assertEquals( "Wrong count for category 'a'!", @@ -424,23 +422,11 @@ public class TestTaxonomyFacetAssociations extends FacetTestCase { Facets facets = new TaxonomyFacetFloatAssociations( "wrong_field", taxoReader, config, fc, AssociationAggregationFunction.SUM); - expectThrows( - IllegalArgumentException.class, - () -> { - facets.getSpecificValue("float"); - }); + expectThrows(IllegalArgumentException.class, () -> facets.getSpecificValue("float")); - expectThrows( - IllegalArgumentException.class, - () -> { - facets.getTopChildren(10, "float"); - }); + expectThrows(IllegalArgumentException.class, () -> facets.getTopChildren(10, "float")); - expectThrows( - IllegalArgumentException.class, - () -> { - facets.getAllChildren("float"); - }); + expectThrows(IllegalArgumentException.class, () -> facets.getAllChildren("float")); } public void testMixedTypesInSameIndexField() throws Exception { @@ -455,10 +441,7 @@ public class TestTaxonomyFacetAssociations extends FacetTestCase { doc.add(new IntAssociationFacetField(14, "a", "x")); doc.add(new FloatAssociationFacetField(55.0f, "b", "y")); expectThrows( - IllegalArgumentException.class, - () -> { - writer.addDocument(config.build(taxoWriter, doc)); - }); + IllegalArgumentException.class, () -> writer.addDocument(config.build(taxoWriter, doc))); writer.close(); IOUtils.close(taxoWriter, dir, taxoDir); } @@ -475,10 +458,7 @@ public class TestTaxonomyFacetAssociations extends FacetTestCase { Document doc = new Document(); doc.add(new IntAssociationFacetField(14, "a", "x")); expectThrows( - IllegalArgumentException.class, - () -> { - writer.addDocument(config.build(taxoWriter, doc)); - }); + IllegalArgumentException.class, () -> writer.addDocument(config.build(taxoWriter, doc))); writer.close(); IOUtils.close(taxoWriter, dir, taxoDir); @@ -496,10 +476,7 @@ public class TestTaxonomyFacetAssociations extends FacetTestCase { Document doc = new Document(); doc.add(new IntAssociationFacetField(14, "a", "x")); expectThrows( - IllegalArgumentException.class, - () -> { - writer.addDocument(config.build(taxoWriter, doc)); - }); + IllegalArgumentException.class, () -> writer.addDocument(config.build(taxoWriter, doc))); writer.close(); IOUtils.close(taxoWriter, dir, taxoDir); @@ -528,9 +505,8 @@ public class TestTaxonomyFacetAssociations extends FacetTestCase { new String[0], 2, -1, - new LabelAndValue[] { - new LabelAndValue("a", 100), new LabelAndValue("b", 150), - }); + new LabelAndValue("a", 100), + new LabelAndValue("b", 150)); assertEquals( "Wrong count for category 'a'!", 100, facets.getSpecificValue("int", "a").intValue()); diff --git a/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestTaxonomyFacetCounts2.java b/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestTaxonomyFacetCounts2.java index 90b08d9120c..213b2d4bdd6 100644 --- a/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestTaxonomyFacetCounts2.java +++ b/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestTaxonomyFacetCounts2.java @@ -122,8 +122,7 @@ public class TestTaxonomyFacetCounts2 extends FacetTestCase { doc.add(new StringField(A.field(), A.text(), Store.NO)); } - private static void addFacets(Document doc, FacetsConfig config, boolean updateTermExpectedCounts) - throws IOException { + private static void addFacets(Document doc, boolean updateTermExpectedCounts) throws IOException { List docCategories = randomCategories(random()); for (FacetField ff : docCategories) { doc.add(ff); @@ -163,29 +162,27 @@ public class TestTaxonomyFacetCounts2 extends FacetTestCase { indexWriter.commit(); // flush a segment } - private static void indexDocsWithFacetsNoTerms( - IndexWriter indexWriter, TaxonomyWriter taxoWriter, Map expectedCounts) + private static void indexDocsWithFacetsNoTerms(IndexWriter indexWriter, TaxonomyWriter taxoWriter) throws IOException { Random random = random(); int numDocs = atLeast(random, 2); FacetsConfig config = getConfig(); for (int i = 0; i < numDocs; i++) { Document doc = new Document(); - addFacets(doc, config, false); + addFacets(doc, false); indexWriter.addDocument(config.build(taxoWriter, doc)); } indexWriter.commit(); // flush a segment } private static void indexDocsWithFacetsAndTerms( - IndexWriter indexWriter, TaxonomyWriter taxoWriter, Map expectedCounts) - throws IOException { + IndexWriter indexWriter, TaxonomyWriter taxoWriter) throws IOException { Random random = random(); int numDocs = atLeast(random, 2); FacetsConfig config = getConfig(); for (int i = 0; i < numDocs; i++) { Document doc = new Document(); - addFacets(doc, config, true); + addFacets(doc, true); addField(doc); indexWriter.addDocument(config.build(taxoWriter, doc)); } @@ -193,8 +190,7 @@ public class TestTaxonomyFacetCounts2 extends FacetTestCase { } private static void indexDocsWithFacetsAndSomeTerms( - IndexWriter indexWriter, TaxonomyWriter taxoWriter, Map expectedCounts) - throws IOException { + IndexWriter indexWriter, TaxonomyWriter taxoWriter) throws IOException { Random random = random(); int numDocs = atLeast(random, 2); FacetsConfig config = getConfig(); @@ -204,7 +200,7 @@ public class TestTaxonomyFacetCounts2 extends FacetTestCase { if (hasContent) { addField(doc); } - addFacets(doc, config, hasContent); + addFacets(doc, hasContent); indexWriter.addDocument(config.build(taxoWriter, doc)); } indexWriter.commit(); // flush a segment @@ -256,13 +252,13 @@ public class TestTaxonomyFacetCounts2 extends FacetTestCase { indexDocsNoFacets(indexWriter); // segment w/ categories, no content - indexDocsWithFacetsNoTerms(indexWriter, taxoWriter, allExpectedCounts); + indexDocsWithFacetsNoTerms(indexWriter, taxoWriter); // segment w/ categories and content - indexDocsWithFacetsAndTerms(indexWriter, taxoWriter, allExpectedCounts); + indexDocsWithFacetsAndTerms(indexWriter, taxoWriter); // segment w/ categories and some content - indexDocsWithFacetsAndSomeTerms(indexWriter, taxoWriter, allExpectedCounts); + indexDocsWithFacetsAndSomeTerms(indexWriter, taxoWriter); indexWriter.close(); IOUtils.close(taxoWriter); diff --git a/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestTaxonomyFacetValueSource.java b/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestTaxonomyFacetValueSource.java index 1ebefe5699f..67113ea38d8 100644 --- a/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestTaxonomyFacetValueSource.java +++ b/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestTaxonomyFacetValueSource.java @@ -134,11 +134,7 @@ public class TestTaxonomyFacetValueSource extends FacetTestCase { // test getTopChildren(0, dim) final Facets f = facets; - expectThrows( - IllegalArgumentException.class, - () -> { - f.getTopChildren(0, "Author"); - }); + expectThrows(IllegalArgumentException.class, () -> f.getTopChildren(0, "Author")); taxoReader.close(); searcher.getIndexReader().close(); @@ -207,11 +203,7 @@ public class TestTaxonomyFacetValueSource extends FacetTestCase { List results = facets.getAllDims(10); // test getAllDims(0) - expectThrows( - IllegalArgumentException.class, - () -> { - facets.getAllDims(0); - }); + expectThrows(IllegalArgumentException.class, () -> facets.getAllDims(0)); assertEquals(3, results.size()); assertEquals( @@ -236,18 +228,10 @@ public class TestTaxonomyFacetValueSource extends FacetTestCase { assertEquals(results, allDimsResults); // test getTopDims(0, 1) - expectThrows( - IllegalArgumentException.class, - () -> { - facets.getTopDims(0, 1); - }); + expectThrows(IllegalArgumentException.class, () -> facets.getTopDims(0, 1)); // test getTopDims(1, 0) with topNChildren = 0 - expectThrows( - IllegalArgumentException.class, - () -> { - facets.getTopDims(1, 0); - }); + expectThrows(IllegalArgumentException.class, () -> facets.getTopDims(1, 0)); IOUtils.close(searcher.getIndexReader(), taxoReader, dir, taxoDir); } @@ -297,17 +281,9 @@ public class TestTaxonomyFacetValueSource extends FacetTestCase { // test default implementation of getTopDims List topDimsResults = facets.getTopDims(10, 10); assertTrue(topDimsResults.isEmpty()); - expectThrows( - IllegalArgumentException.class, - () -> { - facets.getSpecificValue("a"); - }); + expectThrows(IllegalArgumentException.class, () -> facets.getSpecificValue("a")); - expectThrows( - IllegalArgumentException.class, - () -> { - facets.getTopChildren(10, "a"); - }); + expectThrows(IllegalArgumentException.class, () -> facets.getTopChildren(10, "a")); IOUtils.close(searcher.getIndexReader(), taxoReader, dir, taxoDir); } @@ -694,7 +670,7 @@ public class TestTaxonomyFacetValueSource extends FacetTestCase { "dim" + i, new String[0], aggregatedValue, - labelValues.toArray(new LabelAndValue[labelValues.size()]), + labelValues.toArray(new LabelAndValue[0]), labelValues.size())); } } @@ -718,8 +694,8 @@ public class TestTaxonomyFacetValueSource extends FacetTestCase { sortTies(actual); if (VERBOSE) { - System.out.println("expected=\n" + expected.toString()); - System.out.println("actual=\n" + actual.toString()); + System.out.println("expected=\n" + expected); + System.out.println("actual=\n" + actual); } assertFloatValuesEquals(expected, actual); diff --git a/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/directory/TestAddTaxonomy.java b/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/directory/TestAddTaxonomy.java index 88b88c176dd..e0420f16e14 100644 --- a/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/directory/TestAddTaxonomy.java +++ b/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/directory/TestAddTaxonomy.java @@ -43,20 +43,18 @@ public class TestAddTaxonomy extends FacetTestCase { Thread[] addThreads = new Thread[4]; for (int j = 0; j < addThreads.length; j++) { addThreads[j] = - new Thread() { - @Override - public void run() { - Random random = random(); - while (numCats.decrementAndGet() > 0) { - String cat = Integer.toString(random.nextInt(range)); - try { - tw.addCategory(new FacetLabel("a", cat)); - } catch (IOException e) { - throw new RuntimeException(e); + new Thread( + () -> { + Random random = random(); + while (numCats.decrementAndGet() > 0) { + String cat = Integer.toString(random.nextInt(range)); + try { + tw.addCategory(new FacetLabel("a", cat)); + } catch (IOException e) { + throw new RuntimeException(e); + } } - } - } - }; + }); } for (Thread t : addThreads) t.start(); @@ -83,11 +81,9 @@ public class TestAddTaxonomy extends FacetTestCase { } private void validate(Directory dest, Directory src, OrdinalMap ordMap) throws Exception { - DirectoryTaxonomyReader destTR = new DirectoryTaxonomyReader(dest); - try { + try (DirectoryTaxonomyReader destTR = new DirectoryTaxonomyReader(dest)) { final int destSize = destTR.getSize(); - DirectoryTaxonomyReader srcTR = new DirectoryTaxonomyReader(src); - try { + try (DirectoryTaxonomyReader srcTR = new DirectoryTaxonomyReader(src)) { int[] map = ordMap.getMap(); // validate taxo sizes @@ -107,11 +103,7 @@ public class TestAddTaxonomy extends FacetTestCase { assertTrue(cp + " not found in destination", destOrdinal > 0); assertEquals(destOrdinal, map[j]); } - } finally { - srcTR.close(); } - } finally { - destTR.close(); } } @@ -209,19 +201,17 @@ public class TestAddTaxonomy extends FacetTestCase { Directory dest = newDirectory(); final DirectoryTaxonomyWriter destTW = new DirectoryTaxonomyWriter(dest); Thread t = - new Thread() { - @Override - public void run() { - for (int i = 0; i < numCategories; i++) { - try { - destTW.addCategory(new FacetLabel("a", Integer.toString(i))); - } catch (IOException e) { - // shouldn't happen - if it does, let the test fail on uncaught exception. - throw new RuntimeException(e); + new Thread( + () -> { + for (int i = 0; i < numCategories; i++) { + try { + destTW.addCategory(new FacetLabel("a", Integer.toString(i))); + } catch (IOException e) { + // shouldn't happen - if it does, let the test fail on uncaught exception. + throw new RuntimeException(e); + } } - } - } - }; + }); t.start(); OrdinalMap map = new MemoryOrdinalMap(); diff --git a/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/directory/TestConcurrentFacetedIndexing.java b/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/directory/TestConcurrentFacetedIndexing.java index 7efb9bd6ce3..ca71fe4a2fa 100644 --- a/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/directory/TestConcurrentFacetedIndexing.java +++ b/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/directory/TestConcurrentFacetedIndexing.java @@ -109,35 +109,32 @@ public class TestConcurrentFacetedIndexing extends FacetTestCase { for (int i = 0; i < indexThreads.length; i++) { indexThreads[i] = - new Thread() { + new Thread( + () -> { + Random random = random(); + while (numDocs.decrementAndGet() > 0) { + try { + Document doc = new Document(); + int numCats = random.nextInt(3) + 1; // 1-3 + while (numCats-- > 0) { + FacetField ff = newCategory(); + doc.add(ff); - @Override - public void run() { - Random random = random(); - while (numDocs.decrementAndGet() > 0) { - try { - Document doc = new Document(); - int numCats = random.nextInt(3) + 1; // 1-3 - while (numCats-- > 0) { - FacetField ff = newCategory(); - doc.add(ff); - - FacetLabel label = new FacetLabel(ff.dim, ff.path); - // add all prefixes to values - int level = label.length; - while (level > 0) { - String s = FacetsConfig.pathToString(label.components, level); - values.put(s, s); - --level; + FacetLabel label = new FacetLabel(ff.dim, ff.path); + // add all prefixes to values + int level = label.length; + while (level > 0) { + String s = FacetsConfig.pathToString(label.components, level); + values.put(s, s); + --level; + } } + iw.addDocument(config.build(tw, doc)); + } catch (IOException e) { + throw new RuntimeException(e); } - iw.addDocument(config.build(tw, doc)); - } catch (IOException e) { - throw new RuntimeException(e); } - } - } - }; + }); } for (Thread t : indexThreads) t.start(); diff --git a/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/directory/TestDirectoryTaxonomyReader.java b/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/directory/TestDirectoryTaxonomyReader.java index 01fe8cee262..f5ed929ede4 100644 --- a/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/directory/TestDirectoryTaxonomyReader.java +++ b/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/directory/TestDirectoryTaxonomyReader.java @@ -121,11 +121,7 @@ public class TestDirectoryTaxonomyReader extends FacetTestCase { DirectoryTaxonomyReader ltr = new DirectoryTaxonomyReader(dir); ltr.close(); - expectThrows( - AlreadyClosedException.class, - () -> { - ltr.getSize(); - }); + expectThrows(AlreadyClosedException.class, ltr::getSize); dir.close(); } @@ -662,13 +658,13 @@ public class TestDirectoryTaxonomyReader extends FacetTestCase { final int maxNumberOfLabelsToIndex = 1000; final int maxNumberOfUniqueLabelsToIndex = maxNumberOfLabelsToIndex / 2; final int cacheSize = maxNumberOfUniqueLabelsToIndex / 2; // to cause some cache evictions - String randomArray[] = new String[RandomizedTest.randomIntBetween(1, maxNumberOfLabelsToIndex)]; + String[] randomArray = new String[RandomizedTest.randomIntBetween(1, maxNumberOfLabelsToIndex)]; // adding a smaller bound on ints ensures that we will have some duplicate ordinals in random // test cases Arrays.setAll( randomArray, i -> Integer.toString(random().nextInt(maxNumberOfUniqueLabelsToIndex))); - FacetLabel allPaths[] = new FacetLabel[randomArray.length]; + FacetLabel[] allPaths = new FacetLabel[randomArray.length]; for (int i = 0; i < randomArray.length; i++) { allPaths[i] = new FacetLabel(randomArray[i]); @@ -684,7 +680,7 @@ public class TestDirectoryTaxonomyReader extends FacetTestCase { DirectoryTaxonomyReader r1 = new DirectoryTaxonomyReader(src); r1.setCacheSize(cacheSize); - int allOrdinals[] = r1.getBulkOrdinals(allPaths); + int[] allOrdinals = r1.getBulkOrdinals(allPaths); // Assert getPath and getBulkPath first, then assert getOrdinal and getBulkOrdinals. // Create multiple threads to check result correctness and thread contention in the cache. @@ -692,43 +688,43 @@ public class TestDirectoryTaxonomyReader extends FacetTestCase { Thread[] addThreads = new Thread[RandomNumbers.randomIntBetween(random(), 1, 12)]; for (int z = 0; z < addThreads.length; z++) { addThreads[z] = - new Thread() { - @Override - public void run() { - // each thread iterates for numThreadIterations times - int numThreadIterations = random().nextInt(10); - for (int threadIterations = 0; - threadIterations < numThreadIterations; - threadIterations++) { + new Thread( + () -> { + // each thread iterates for numThreadIterations times + int numThreadIterations = random().nextInt(10); + for (int threadIterations = 0; + threadIterations < numThreadIterations; + threadIterations++) { - // length of the FacetLabel array that we are going to check - int numOfOrdinalsToCheck = RandomizedTest.randomIntBetween(1, allOrdinals.length); - int[] ordinals = new int[numOfOrdinalsToCheck]; - FacetLabel[] path = new FacetLabel[numOfOrdinalsToCheck]; + // length of the FacetLabel array that we are going to check + int numOfOrdinalsToCheck = + RandomizedTest.randomIntBetween(1, allOrdinals.length); + int[] ordinals = new int[numOfOrdinalsToCheck]; + FacetLabel[] path = new FacetLabel[numOfOrdinalsToCheck]; - for (int i = 0; i < numOfOrdinalsToCheck; i++) { - // we deliberately allow it to choose repeat indexes as this will exercise the - // cache - int ordinalIndex = random().nextInt(allOrdinals.length); - ordinals[i] = allOrdinals[ordinalIndex]; - path[i] = allPaths[ordinalIndex]; - } - - try { - // main check for correctness is done here - if (assertGettingOrdinals) { - assertGettingOrdinals(r1, ordinals, path); - } else { - assertGettingPaths(r1, path, ordinals); + for (int i = 0; i < numOfOrdinalsToCheck; i++) { + // we deliberately allow it to choose repeat indexes as this will exercise the + // cache + int ordinalIndex = random().nextInt(allOrdinals.length); + ordinals[i] = allOrdinals[ordinalIndex]; + path[i] = allPaths[ordinalIndex]; + } + + try { + // main check for correctness is done here + if (assertGettingOrdinals) { + assertGettingOrdinals(r1, ordinals, path); + } else { + assertGettingPaths(r1, path, ordinals); + } + } catch (IOException e) { + // this should ideally never occur, but if it does just rethrow the error to + // the + // caller + throw new RuntimeException(e); } - } catch (IOException e) { - // this should ideally never occur, but if it does just rethrow the error to the - // caller - throw new RuntimeException(e); } - } - } - }; + }); } for (Thread t : addThreads) t.start(); for (Thread t : addThreads) t.join(); diff --git a/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/directory/TestDirectoryTaxonomyWriter.java b/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/directory/TestDirectoryTaxonomyWriter.java index dd55e55a22d..7350f0bffdb 100644 --- a/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/directory/TestDirectoryTaxonomyWriter.java +++ b/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/directory/TestDirectoryTaxonomyWriter.java @@ -318,37 +318,35 @@ public class TestDirectoryTaxonomyWriter extends FacetTestCase { Thread[] addThreads = new Thread[RandomNumbers.randomIntBetween(random(), 1, 12)]; for (int z = 0; z < addThreads.length; z++) { addThreads[z] = - new Thread() { - @Override - public void run() { - Random random = random(); - while (numCats.decrementAndGet() > 0) { - try { - int value = random.nextInt(range); - FacetLabel cp = - new FacetLabel( - Integer.toString(value / 1000), - Integer.toString(value / 10000), - Integer.toString(value / 100000), - Integer.toString(value)); - int ord = tw.addCategory(cp); - assertTrue( - "invalid parent for ordinal " + ord + ", category " + cp, - tw.getParent(ord) != -1); - String l1 = FacetsConfig.pathToString(cp.components, 1); - String l2 = FacetsConfig.pathToString(cp.components, 2); - String l3 = FacetsConfig.pathToString(cp.components, 3); - String l4 = FacetsConfig.pathToString(cp.components, 4); - values.put(l1, l1); - values.put(l2, l2); - values.put(l3, l3); - values.put(l4, l4); - } catch (IOException e) { - throw new RuntimeException(e); + new Thread( + () -> { + Random random = random(); + while (numCats.decrementAndGet() > 0) { + try { + int value = random.nextInt(range); + FacetLabel cp = + new FacetLabel( + Integer.toString(value / 1000), + Integer.toString(value / 10000), + Integer.toString(value / 100000), + Integer.toString(value)); + int ord = tw.addCategory(cp); + assertTrue( + "invalid parent for ordinal " + ord + ", category " + cp, + tw.getParent(ord) != -1); + String l1 = FacetsConfig.pathToString(cp.components, 1); + String l2 = FacetsConfig.pathToString(cp.components, 2); + String l3 = FacetsConfig.pathToString(cp.components, 3); + String l4 = FacetsConfig.pathToString(cp.components, 4); + values.put(l1, l1); + values.put(l2, l2); + values.put(l3, l3); + values.put(l4, l4); + } catch (IOException e) { + throw new RuntimeException(e); + } } - } - } - }; + }); } for (Thread t : addThreads) t.start();