From ecd28bd20bf8d2ac3645b88e936b996d9cc627d3 Mon Sep 17 00:00:00 2001 From: Jan Faber Date: Thu, 1 Mar 2018 16:49:44 +0100 Subject: [PATCH 01/13] switch from es 2.x to es 6.x, switch to es test framework --- build.gradle | 129 +++++------ gradle.properties | 8 + gradle/ext.gradle | 16 ++ gradle/git.gradle | 9 - gradle/publish.gradle | 47 +--- gradle/sonarqube.gradle | 39 ++++ gradle/wrapper/gradle-wrapper.jar | Bin 53319 -> 54708 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 26 ++- gradlew.bat | 6 - settings.gradle | 1 - .../BaseformAnalysisBinderProcessor.java | 11 - .../baseform/BaseformTokenFilter.java | 60 +++++- .../BaseformTokenFilterAnalysisProvider.java | 25 +++ .../baseform/BaseformTokenFilterFactory.java | 21 +- .../baseform/AnalysisBaseformPlugin.java | 44 ++-- .../analysis/BaseformTokenFilterTests.java | 55 +++-- .../index/analysis/DictionaryTests.java | 8 +- .../EnglishBaseformTokenFilterTests.java | 47 ++-- .../GermanBaseformTokenFilterTests.java | 42 ++-- .../index/analysis/MapperTestUtils.java | 204 ------------------ .../plugin/baseform/BaseformPluginTest.java | 19 -- .../plugin/baseform/MockNode.java | 35 --- .../plugin/baseform/NodeTestUtils.java | 196 ----------------- 24 files changed, 352 insertions(+), 699 deletions(-) create mode 100644 gradle.properties create mode 100644 gradle/ext.gradle delete mode 100644 gradle/git.gradle create mode 100644 gradle/sonarqube.gradle delete mode 100644 settings.gradle delete mode 100644 src/main/java/org/xbib/elasticsearch/index/analysis/baseform/BaseformAnalysisBinderProcessor.java create mode 100644 src/main/java/org/xbib/elasticsearch/index/analysis/baseform/BaseformTokenFilterAnalysisProvider.java delete mode 100644 src/test/java/org/xbib/elasticsearch/index/analysis/MapperTestUtils.java delete mode 100644 src/test/java/org/xbib/elasticsearch/plugin/baseform/BaseformPluginTest.java delete mode 100644 src/test/java/org/xbib/elasticsearch/plugin/baseform/MockNode.java delete mode 100644 src/test/java/org/xbib/elasticsearch/plugin/baseform/NodeTestUtils.java diff --git a/build.gradle b/build.gradle index 0091f1e..d187709 100644 --- a/build.gradle +++ b/build.gradle @@ -1,60 +1,40 @@ -group = 'org.xbib.elasticsearch.plugin' -version = '2.2.0.0' - -ext { - pluginName = 'baseform' - pluginClassname = 'org.xbib.elasticsearch.plugin.baseform.AnalysisBaseformPlugin' - pluginDescription = 'Baseform plugin for Elasticsearch' - user = 'jprante' - name = 'elasticsearch-analysis-baseform' - scmUrl = 'https://github.com/' + user + '/' + name - scmConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git' - scmDeveloperConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git' - versions = [ - 'elasticsearch' : '2.2.0', - 'log4j': '2.5', - 'junit' : '4.12' - ] -} -println "Host: " + java.net.InetAddress.getLocalHost() -println "Gradle: " + gradle.gradleVersion + " JVM: " + org.gradle.internal.jvm.Jvm.current() + " Groovy: " + GroovySystem.getVersion() -println "Build: group: '${project.group}', name: '${project.name}', version: '${project.version}'" -println "Timestamp: " + java.time.Instant.now().atZone(java.time.ZoneId.systemDefault()).toString() - -buildscript { - repositories { - mavenLocal() - mavenCentral() - jcenter() - maven { - url "http://xbib.org/repository" - } - } - dependencies { - classpath 'org.ajoberstar:gradle-git:1.4.2' - classpath 'co.riiid:gradle-github-plugin:0.4.2' - classpath 'io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.5.3' - } +plugins { + id "org.sonarqube" version "2.5" + id "org.xbib.gradle.plugin.asciidoctor" version "1.5.4.1.0" + id "io.codearte.nexus-staging" version "0.7.0" } + +printf "Host: %s\nOS: %s %s %s\nJVM: %s %s %s %s\nGroovy: %s\nGradle: %s\n" + + "Build: group: ${project.group} name: ${project.name} version: ${project.version}\n", + InetAddress.getLocalHost(), + System.getProperty("os.name"), + System.getProperty("os.arch"), + System.getProperty("os.version"), + System.getProperty("java.version"), + System.getProperty("java.vm.version"), + System.getProperty("java.vm.vendor"), + System.getProperty("java.vm.name"), + GroovySystem.getVersion(), + gradle.gradleVersion + apply plugin: 'java' apply plugin: 'maven' apply plugin: 'signing' -apply plugin: 'co.riiid.gradle' +apply plugin: 'findbugs' +apply plugin: 'pmd' +apply plugin: 'checkstyle' +apply plugin: "jacoco" +apply plugin: 'org.xbib.gradle.plugin.asciidoctor' repositories { mavenCentral() - mavenLocal() - jcenter() - maven { - url "http://xbib.org/repository" - } } configurations { wagon - releaseJars { + distJars { extendsFrom runtime exclude group: 'org.elasticsearch' exclude module: 'lucene-core' @@ -63,27 +43,41 @@ configurations { exclude module: 'jackson-core' exclude module: 'jackson-dataformat-smile' exclude module: 'jackson-dataformat-yaml' + exclude module: 'log4j-api' } } +apply from: 'gradle/ext.gradle' +apply from: 'gradle/publish.gradle' +apply from: 'gradle/sonarqube.gradle' + dependencies { - compile "org.elasticsearch:elasticsearch:${versions.elasticsearch}" - testCompile "junit:junit:${versions.junit}" - testCompile "org.apache.logging.log4j:log4j-slf4j-impl:${versions.log4j}" - testCompile "org.apache.logging.log4j:log4j-core:${versions.log4j}" - releaseJars "${project.group}:${project.name}:${project.version}" - wagon 'org.apache.maven.wagon:wagon-ssh-external:2.10' + def without_hamcrest = { + exclude group: 'org.hamcrest', module: 'hamcrest-core' + } + compile "org.elasticsearch:elasticsearch:${project.property('elasticsearch.version')}" + compile "org.apache.logging.log4j:log4j-api:${project.property('log4j.version')}" + testCompile "junit:junit:${project.property('junit.version')}", without_hamcrest + testCompile "org.apache.logging.log4j:log4j-core:${project.property('log4j.version')}" + testCompile "org.elasticsearch.plugin:transport-netty4-client:${project.property('elasticsearch.version')}" + testCompile "org.elasticsearch.test:framework:${project.property('elasticsearch.version')}" + testCompile "org.codelibs.elasticsearch.module:analysis-common:${project.property('elasticsearch.version')}" + distJars "${project.group}:${project.name}:${project.version}" + wagon "org.apache.maven.wagon:wagon-ssh:${project.property('wagon.version')}" } -sourceCompatibility = 1.7 -targetCompatibility = 1.7 +sourceCompatibility = JavaVersion.VERSION_1_8 +targetCompatibility = JavaVersion.VERSION_1_8 +[compileJava, compileTestJava]*.options*.encoding = 'UTF-8' tasks.withType(JavaCompile) { - options.compilerArgs << "-Xlint:unchecked,deprecation" + options.compilerArgs << "-Xlint:all" << "-profile" << "compact2" } test { - systemProperty 'path.home', projectDir.absolutePath + systemProperties['path.home'] = System.getProperty("user.dir") + systemProperties['tests.security.manager'] = false + testLogging { showStandardStreams = false exceptionFormat = 'full' @@ -95,28 +89,26 @@ task makePluginDescriptor(type: Copy) { into 'build/tmp/plugin' expand([ 'descriptor': [ - 'name': pluginName, - 'classname': pluginClassname, - 'description': pluginDescription, - 'jvm': true, - 'site': false, - 'isolated': true, - 'version': project.property('version'), - 'javaVersion': project.property('targetCompatibility'), - 'elasticsearchVersion' : versions.elasticsearch + 'name': pluginName, + 'classname': pluginClassname, + 'description': pluginDescription, + 'version': project.property('version'), + 'javaVersion': project.property('targetCompatibility'), + 'elasticsearchVersion' : project.property('elasticsearch.version') ] ]) } task buildPluginZip(type: Zip, dependsOn: [':jar', ':makePluginDescriptor']) { - from configurations.releaseJars + from configurations.distJars from 'build/tmp/plugin' + into 'elasticsearch' classifier = 'plugin' } task unpackPlugin(type: Copy, dependsOn: [':buildPluginZip']) { delete "plugins" - from configurations.releaseJars + from configurations.distJars from 'build/tmp/plugin' into "plugins/${pluginName}" } @@ -140,7 +132,7 @@ task sourcesJar(type: Jar, dependsOn: classes) { } artifacts { - archives javadocJar, sourcesJar, buildPluginZip + archives sourcesJar, javadocJar, buildPluginZip } if (project.hasProperty('signing.keyId')) { @@ -148,8 +140,3 @@ if (project.hasProperty('signing.keyId')) { sign configurations.archives } } - -ext.grgit = org.ajoberstar.grgit.Grgit.open() - -apply from: 'gradle/git.gradle' -apply from: 'gradle/publish.gradle' diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..1d34262 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,8 @@ +group = org.xbib.elasticsearch.plugin +name = elasticsearch-analysis-baseform +version = 6.1.1.0 + +elasticsearch.version = 6.1.1 +log4j.version = 2.9.1 +junit.version = 4.12 +wagon.version = 2.12 diff --git a/gradle/ext.gradle b/gradle/ext.gradle new file mode 100644 index 0000000..b768e90 --- /dev/null +++ b/gradle/ext.gradle @@ -0,0 +1,16 @@ + +ext { + pluginName = 'decompound' + pluginClassname = 'org.xbib.elasticsearch.plugin.analysis.decompound.AnalysisDecompoundPlugin' + pluginDescription = 'Decompounder plugin for Elasticsearch' + user = 'jprante' + name = 'elasticsearch-analysis-decompound' + scmUrl = 'https://github.com/' + user + '/' + name + scmConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git' + scmDeveloperConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git' + versions = [ + 'elasticsearch' : '5.1.1', + 'log4j': '2.5', + 'junit' : '4.12' + ] +} diff --git a/gradle/git.gradle b/gradle/git.gradle deleted file mode 100644 index 6043d07..0000000 --- a/gradle/git.gradle +++ /dev/null @@ -1,9 +0,0 @@ - -task gitRelease(dependsOn: build) << { - grgit.add(patterns: ['.'], update: true) - grgit.commit(message: "release of ${project.version}") - grgit.tag.remove(names: [project.version]) - grgit.tag.add(name: project.version) - grgit.push() - grgit.push(tags: true) -} \ No newline at end of file diff --git a/gradle/publish.gradle b/gradle/publish.gradle index 28d91c2..0e648cb 100644 --- a/gradle/publish.gradle +++ b/gradle/publish.gradle @@ -1,19 +1,3 @@ -apply plugin: 'io.codearte.nexus-staging' - -/* -nexus { - attachJavadoc = true - attachSources = true - attachTests = true - sign = true - repositoryUrl = 'https://oss.sonatype.org/service/local/staging/deploy/maven2' - snapshotRepositoryUrl = 'https://oss.sonatype.org/content/repositories/snapshots' -} -*/ - -nexusStaging { - packageGroup = "org.xbib" -} task xbibUpload(type: Upload) { configuration = configurations.archives @@ -22,27 +6,25 @@ task xbibUpload(type: Upload) { if (project.hasProperty("xbibUsername")) { mavenDeployer { configuration = configurations.wagon - repository(url: 'scpexe://xbib.org/repository') { - authentication(userName: xbibUsername, privateKey: xbibPrivateKey) + repository(url: uri('sftp://xbib.org/repository')) { + authentication(userName: xbibUsername, privateKey: xbibPrivateKey) } } } } } -task mavenCentralUpload(type: Upload) { +task sonaTypeUpload(type: Upload) { configuration = configurations.archives uploadDescriptor = true repositories { if (project.hasProperty('ossrhUsername')) { mavenDeployer { - beforeDeployment { - MavenDeployment deployment -> signing.signPom(deployment) - } - repository(url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2') { + beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } + repository(url: uri(ossrhReleaseUrl)) { authentication(userName: ossrhUsername, password: ossrhPassword) } - snapshotRepository(url: 'https://oss.sonatype.org/content/repositories/snapshots') { + snapshotRepository(url: uri(ossrhSnapshotUrl)) { authentication(userName: ossrhUsername, password: ossrhPassword) } pom.project { @@ -79,20 +61,3 @@ task mavenCentralUpload(type: Upload) { } } } - -if (project.hasProperty('githubToken')) { - github { - owner = user - token = githubToken - repo = project.name - name = project.version - tagName = project.version - targetCommitish = 'master' - assets = [ - "build/distributions/${project.name}-${project.version}-plugin.zip" - ] - } - githubRelease { - dependsOn gitRelease, buildPluginZip - } -} \ No newline at end of file diff --git a/gradle/sonarqube.gradle b/gradle/sonarqube.gradle new file mode 100644 index 0000000..3985a4f --- /dev/null +++ b/gradle/sonarqube.gradle @@ -0,0 +1,39 @@ +tasks.withType(FindBugs) { + ignoreFailures = true + reports { + xml.enabled = false + html.enabled = true + } +} +tasks.withType(Pmd) { + ignoreFailures = true + reports { + xml.enabled = true + html.enabled = true + } +} +tasks.withType(Checkstyle) { + ignoreFailures = true + reports { + xml.enabled = true + html.enabled = true + } +} + +jacocoTestReport { + reports { + xml.enabled = true + csv.enabled = false + } +} + +sonarqube { + properties { + property "sonar.projectName", "${project.group} ${project.name}" + property "sonar.sourceEncoding", "UTF-8" + property "sonar.tests", "src/test/java" + property "sonar.scm.provider", "git" + property "sonar.java.coveragePlugin", "jacoco" + property "sonar.junit.reportsPath", "build/test-results/test/" + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d3b83982b9b1bccad955349d702be9b884c6e049..7a3265ee94c0ab25cf079ac8ccdf87f41d455d42 100644 GIT binary patch delta 25563 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 delta 24167 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 K`jG<&(EkDPr&!Mb diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 120a40f..f16d266 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Wed Jun 22 12:35:29 CEST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-bin.zip diff --git a/gradlew b/gradlew index 27309d9..cccdd3d 100755 --- a/gradlew +++ b/gradlew @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh ############################################################################## ## @@ -33,11 +33,11 @@ DEFAULT_JVM_OPTS="" # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -154,11 +154,19 @@ if $cygwin ; then esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=$(save "$@") -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index f6d5974..e95643d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -49,7 +49,6 @@ goto fail @rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. @@ -60,11 +59,6 @@ set _SKIP=2 if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ :execute @rem Setup the command line diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index a11be14..0000000 --- a/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'elasticsearch-analysis-baseform' diff --git a/src/main/java/org/xbib/elasticsearch/index/analysis/baseform/BaseformAnalysisBinderProcessor.java b/src/main/java/org/xbib/elasticsearch/index/analysis/baseform/BaseformAnalysisBinderProcessor.java deleted file mode 100644 index ab8e100..0000000 --- a/src/main/java/org/xbib/elasticsearch/index/analysis/baseform/BaseformAnalysisBinderProcessor.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.xbib.elasticsearch.index.analysis.baseform; - -import org.elasticsearch.index.analysis.AnalysisModule; - -public class BaseformAnalysisBinderProcessor extends AnalysisModule.AnalysisBinderProcessor { - - @Override - public void processTokenFilters(TokenFiltersBindings tokenFiltersBindings) { - tokenFiltersBindings.processTokenFilter("baseform", BaseformTokenFilterFactory.class); - } -} diff --git a/src/main/java/org/xbib/elasticsearch/index/analysis/baseform/BaseformTokenFilter.java b/src/main/java/org/xbib/elasticsearch/index/analysis/baseform/BaseformTokenFilter.java index 06dc5a8..f1a7f4f 100644 --- a/src/main/java/org/xbib/elasticsearch/index/analysis/baseform/BaseformTokenFilter.java +++ b/src/main/java/org/xbib/elasticsearch/index/analysis/baseform/BaseformTokenFilter.java @@ -1,5 +1,14 @@ package org.xbib.elasticsearch.index.analysis.baseform; +import java.io.IOException; +import java.nio.charset.CharacterCodingException; +import java.util.LinkedList; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.apache.lucene.analysis.TokenFilter; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; @@ -8,12 +17,20 @@ import org.apache.lucene.util.AttributeSource; import org.xbib.elasticsearch.common.fsa.Dictionary; -import java.io.IOException; -import java.nio.charset.CharacterCodingException; -import java.util.LinkedList; - public class BaseformTokenFilter extends TokenFilter { + private static final Logger LOG = LogManager.getLogger(BaseformTokenFilter.class); + + private static ConcurrentHashMap TERM_CACHE; + + private static final AtomicLong termCacheCount = new AtomicLong(0); + + private static final AtomicBoolean needsClearCache = new AtomicBoolean(false); + + private static long MAX_CACHE_SIZE; + + private static final CharSequence NO_TERMS = ""; + private final LinkedList tokens; private final Dictionary dictionary; @@ -24,10 +41,13 @@ public class BaseformTokenFilter extends TokenFilter { private AttributeSource.State current; - protected BaseformTokenFilter(TokenStream input, Dictionary dictionary) { + protected BaseformTokenFilter(TokenStream input, Dictionary dictionary, long maxCacheSize) { super(input); this.tokens = new LinkedList<>(); - this.dictionary = dictionary; + this.dictionary = dictionary; if (TERM_CACHE == null) { + TERM_CACHE = new ConcurrentHashMap(); + MAX_CACHE_SIZE = maxCacheSize; + } } @Override @@ -53,13 +73,39 @@ public final boolean incrementToken() throws IOException { protected void baseform() throws CharacterCodingException { CharSequence term = new String(termAtt.buffer(), 0, termAtt.length()); - CharSequence s = dictionary.lookup(term); + if (needsClearCache.get()) { + checkCacheSize(); + } + CharSequence s = TERM_CACHE.computeIfAbsent(term, t -> { + if (termCacheCount.incrementAndGet() > MAX_CACHE_SIZE) { + needsClearCache.set(true); + } + + try { + CharSequence baseform = dictionary.lookup(t); + if (baseform == null) { + return NO_TERMS; + } + return baseform; + } catch (CharacterCodingException e) { + return NO_TERMS; + } + }); if (s != null && s.length() > 0) { PackedTokenAttributeImpl impl = new PackedTokenAttributeImpl(); impl.append(s); tokens.add(impl); } } + + private void checkCacheSize() { + needsClearCache.set(false); + final Runtime runtime = Runtime.getRuntime(); + long memoryUsage = runtime.totalMemory() - runtime.freeMemory(); + TERM_CACHE = new ConcurrentHashMap(); + termCacheCount.set(0); + LOG.warn("Clearing term cache for baseform, memory usage: " + memoryUsage); + } @Override public void reset() throws IOException { diff --git a/src/main/java/org/xbib/elasticsearch/index/analysis/baseform/BaseformTokenFilterAnalysisProvider.java b/src/main/java/org/xbib/elasticsearch/index/analysis/baseform/BaseformTokenFilterAnalysisProvider.java new file mode 100644 index 0000000..be25dc0 --- /dev/null +++ b/src/main/java/org/xbib/elasticsearch/index/analysis/baseform/BaseformTokenFilterAnalysisProvider.java @@ -0,0 +1,25 @@ +package org.xbib.elasticsearch.index.analysis.baseform; + +import java.io.IOException; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.analysis.TokenFilterFactory; +import org.elasticsearch.indices.analysis.AnalysisModule.AnalysisProvider; + +public class BaseformTokenFilterAnalysisProvider implements AnalysisProvider{ + + private final long maxBaseformEntries; + + public BaseformTokenFilterAnalysisProvider(long maxBaseformEntries) { + this.maxBaseformEntries = maxBaseformEntries; + } + + @Override + public TokenFilterFactory get(IndexSettings indexSettings, Environment environment, String name, Settings settings) + throws IOException { + return new BaseformTokenFilterFactory(indexSettings, name, settings, maxBaseformEntries); + } + +} diff --git a/src/main/java/org/xbib/elasticsearch/index/analysis/baseform/BaseformTokenFilterFactory.java b/src/main/java/org/xbib/elasticsearch/index/analysis/baseform/BaseformTokenFilterFactory.java index 6589730..3bb76bd 100644 --- a/src/main/java/org/xbib/elasticsearch/index/analysis/baseform/BaseformTokenFilterFactory.java +++ b/src/main/java/org/xbib/elasticsearch/index/analysis/baseform/BaseformTokenFilterFactory.java @@ -1,34 +1,33 @@ package org.xbib.elasticsearch.index.analysis.baseform; +import java.io.IOException; +import java.io.InputStreamReader; + import org.apache.lucene.analysis.TokenStream; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.inject.assistedinject.Assisted; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.index.Index; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.analysis.AbstractTokenFilterFactory; -import org.elasticsearch.index.settings.IndexSettingsService; import org.xbib.elasticsearch.common.fsa.Dictionary; -import java.io.IOException; -import java.io.InputStreamReader; - public class BaseformTokenFilterFactory extends AbstractTokenFilterFactory { private final Dictionary dictionary; + private final long maxCacheSize; + @Inject - public BaseformTokenFilterFactory(Index index, - IndexSettingsService indexSettingsService, - @Assisted String name, - @Assisted Settings settings) { - super(index, indexSettingsService.indexSettings(), name, settings); + public BaseformTokenFilterFactory(IndexSettings indexSettings, @Assisted String name, @Assisted Settings settings, long maxCacheSize) { + super(indexSettings, name, settings); this.dictionary = createDictionary(settings); + this.maxCacheSize = maxCacheSize; } @Override public TokenStream create(TokenStream tokenStream) { - return new BaseformTokenFilter(tokenStream, dictionary); + return new BaseformTokenFilter(tokenStream, dictionary, maxCacheSize); } private Dictionary createDictionary(Settings settings) { diff --git a/src/main/java/org/xbib/elasticsearch/plugin/analysis/baseform/AnalysisBaseformPlugin.java b/src/main/java/org/xbib/elasticsearch/plugin/analysis/baseform/AnalysisBaseformPlugin.java index 7a52e4a..841b086 100644 --- a/src/main/java/org/xbib/elasticsearch/plugin/analysis/baseform/AnalysisBaseformPlugin.java +++ b/src/main/java/org/xbib/elasticsearch/plugin/analysis/baseform/AnalysisBaseformPlugin.java @@ -1,33 +1,45 @@ package org.xbib.elasticsearch.plugin.analysis.baseform; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.index.analysis.AnalysisModule; +import org.elasticsearch.index.analysis.TokenFilterFactory; +import org.elasticsearch.indices.analysis.AnalysisModule.AnalysisProvider; +import org.elasticsearch.plugins.AnalysisPlugin; import org.elasticsearch.plugins.Plugin; -import org.xbib.elasticsearch.index.analysis.baseform.BaseformAnalysisBinderProcessor; +import org.xbib.elasticsearch.index.analysis.baseform.BaseformTokenFilterAnalysisProvider; + +public class AnalysisBaseformPlugin extends Plugin implements AnalysisPlugin { -public class AnalysisBaseformPlugin extends Plugin { + private static final Logger LOG = LogManager.getLogger(AnalysisBaseformPlugin.class); - private final Settings settings; + public static final Setting SETTING_MAX_CACHE_SIZE = + Setting.longSetting("baseform_max_cache_size", 8388608, 131072, Setting.Property.NodeScope); + private final long maxCacheSize; + @Inject public AnalysisBaseformPlugin(Settings settings) { - this.settings = settings; + this.maxCacheSize = SETTING_MAX_CACHE_SIZE.get(settings); + LOG.info("Maximum Cache Size AnalysisBaseformPlugin: " + this.maxCacheSize); + } @Override - public String name() { - return "analysis-baseform"; + public Map> getTokenFilters() { + return Collections.singletonMap("baseform", new BaseformTokenFilterAnalysisProvider(this.maxCacheSize)); } @Override - public String description() { - return "A baseform token filter for german and other languages"; - } - - public void onModule(AnalysisModule module) { - if (settings.getAsBoolean("plugins.baseform.enabled", true)) { - module.addProcessor(new BaseformAnalysisBinderProcessor()); - } - } + public List> getSettings() { + return Stream.of(SETTING_MAX_CACHE_SIZE).collect(Collectors.toList()); + } } diff --git a/src/test/java/org/xbib/elasticsearch/index/analysis/BaseformTokenFilterTests.java b/src/test/java/org/xbib/elasticsearch/index/analysis/BaseformTokenFilterTests.java index 45d729a..b3eb26c 100644 --- a/src/test/java/org/xbib/elasticsearch/index/analysis/BaseformTokenFilterTests.java +++ b/src/test/java/org/xbib/elasticsearch/index/analysis/BaseformTokenFilterTests.java @@ -1,20 +1,22 @@ package org.xbib.elasticsearch.index.analysis; +import java.io.IOException; +import java.io.StringReader; + import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.Tokenizer; import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; - -import org.elasticsearch.index.analysis.AnalysisService; +import org.elasticsearch.Version; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.analysis.TokenFilterFactory; - -import org.junit.Assert; +import org.elasticsearch.test.ESTestCase; import org.junit.Test; +import org.xbib.elasticsearch.plugin.analysis.baseform.AnalysisBaseformPlugin; -import java.io.IOException; -import java.io.StringReader; - -public class BaseformTokenFilterTests extends Assert { - +public class BaseformTokenFilterTests extends ESTestCase { + @Test public void testOne() throws IOException { @@ -44,9 +46,9 @@ public void testOne() throws IOException { "gekostet", "kosten" }; - AnalysisService analysisService = MapperTestUtils.analysisService(); - TokenFilterFactory tokenFilter = analysisService.tokenFilter("baseform"); - Tokenizer tokenizer = analysisService.tokenizer("standard").create(); + TestAnalysis analysis = createTestAnalysis(); + TokenFilterFactory tokenFilter = analysis.tokenFilter.get("baseform"); + Tokenizer tokenizer = analysis.tokenizer.get("standard").create(); tokenizer.setReader(new StringReader(source)); assertSimpleTSOutput(tokenFilter.create(tokenizer), expected); } @@ -70,9 +72,9 @@ public void testTwo() throws IOException { "transportieren", "transportieren" }; - AnalysisService analysisService = MapperTestUtils.analysisService(); - TokenFilterFactory tokenFilter = analysisService.tokenFilter("baseform"); - Tokenizer tokenizer = analysisService.tokenizer("standard").create(); + TestAnalysis analysis = createTestAnalysis(); + TokenFilterFactory tokenFilter = analysis.tokenFilter.get("baseform"); + Tokenizer tokenizer = analysis.tokenizer.get("standard").create(); tokenizer.setReader(new StringReader(source)); assertSimpleTSOutput(tokenFilter.create(tokenizer), expected); } @@ -93,12 +95,29 @@ public void testThree() throws IOException { "gemacht", "machen" }; - AnalysisService analysisService = MapperTestUtils.analysisService(); - TokenFilterFactory tokenFilter = analysisService.tokenFilter("baseform"); - Tokenizer tokenizer = analysisService.tokenizer("standard").create(); + TestAnalysis analysis = createTestAnalysis(); + TokenFilterFactory tokenFilter = analysis.tokenFilter.get("baseform"); + Tokenizer tokenizer = analysis.tokenizer.get("standard").create(); tokenizer.setReader(new StringReader(source)); assertSimpleTSOutput(tokenFilter.create(tokenizer), expected); } + + private TestAnalysis createTestAnalysis() throws IOException { + Settings settings = Settings.builder() + .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) + .build(); + IndexMetaData indexMetaData = IndexMetaData.builder("test") + .settings(settings) + .numberOfShards(1) + .numberOfReplicas(1) + .build(); + Settings nodeSettings = Settings.builder() + .put(AnalysisBaseformPlugin.SETTING_MAX_CACHE_SIZE.getKey(), 131072) + .put("path.home", System.getProperty("path.home", "/tmp")) + .build(); + TestAnalysis analysis = createTestAnalysis(new IndexSettings(indexMetaData, nodeSettings), nodeSettings, new AnalysisBaseformPlugin(nodeSettings)); + return analysis; + } private void assertSimpleTSOutput(TokenStream stream, String[] expected) throws IOException { stream.reset(); diff --git a/src/test/java/org/xbib/elasticsearch/index/analysis/DictionaryTests.java b/src/test/java/org/xbib/elasticsearch/index/analysis/DictionaryTests.java index b69768e..d71496d 100644 --- a/src/test/java/org/xbib/elasticsearch/index/analysis/DictionaryTests.java +++ b/src/test/java/org/xbib/elasticsearch/index/analysis/DictionaryTests.java @@ -1,14 +1,14 @@ package org.xbib.elasticsearch.index.analysis; -import org.junit.Assert; -import org.junit.Test; -import org.xbib.elasticsearch.common.fsa.Dictionary; - import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.CharacterCodingException; +import org.junit.Assert; +import org.junit.Test; +import org.xbib.elasticsearch.common.fsa.Dictionary; + public class DictionaryTests extends Assert { @Test diff --git a/src/test/java/org/xbib/elasticsearch/index/analysis/EnglishBaseformTokenFilterTests.java b/src/test/java/org/xbib/elasticsearch/index/analysis/EnglishBaseformTokenFilterTests.java index 0f13f1c..f3c4ba2 100644 --- a/src/test/java/org/xbib/elasticsearch/index/analysis/EnglishBaseformTokenFilterTests.java +++ b/src/test/java/org/xbib/elasticsearch/index/analysis/EnglishBaseformTokenFilterTests.java @@ -1,23 +1,20 @@ package org.xbib.elasticsearch.index.analysis; +import java.io.IOException; + import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; import org.elasticsearch.Version; +import org.elasticsearch.analysis.common.CommonAnalysisPlugin; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.index.analysis.AnalysisService; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.analysis.NamedAnalyzer; +import org.elasticsearch.test.ESTestCase; import org.junit.Test; +import org.xbib.elasticsearch.plugin.analysis.baseform.AnalysisBaseformPlugin; -import java.io.IOException; -import java.io.InputStreamReader; - -import static org.elasticsearch.common.io.Streams.copyToString; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -public class EnglishBaseformTokenFilterTests { +public class EnglishBaseformTokenFilterTests extends ESTestCase { @Test public void test1() throws IOException { @@ -122,22 +119,28 @@ public void test1() throws IOException { "character", "today" }; - Settings settings = Settings.settingsBuilder() - .loadFromSource(copyToStringFromClasspath("/org/xbib/elasticsearch/index/analysis/baseform_en.json")) - .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) - .put("path.home", System.getProperty("path.home")) - .put("client.type", "node") - .build(); - AnalysisService analysisService = MapperTestUtils.analysisService(settings); - - NamedAnalyzer analyzer = analysisService.analyzer("baseform"); - + TestAnalysis analysis = createTestAnalysis("org/xbib/elasticsearch/index/analysis/baseform_en.json"); + NamedAnalyzer analyzer = analysis.indexAnalyzers.get("baseform"); assertSimpleTSOutput(analyzer.tokenStream("content", source), expected); } - private static String copyToStringFromClasspath(String path) throws IOException { - return copyToString(new InputStreamReader(EnglishBaseformTokenFilterTests.class.getResourceAsStream(path), "UTF-8")); + private TestAnalysis createTestAnalysis(String resource) throws IOException { + Settings settings = Settings.builder() + .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) + .loadFromStream(resource, ClassLoader.getSystemClassLoader().getResourceAsStream(resource), false) + .build(); + IndexMetaData indexMetaData = IndexMetaData.builder("test") + .settings(settings) + .numberOfShards(1) + .numberOfReplicas(1) + .build(); + Settings nodeSettings = Settings.builder() + .put(AnalysisBaseformPlugin.SETTING_MAX_CACHE_SIZE.getKey(), 131072) + .put("path.home", System.getProperty("path.home", "/tmp")) + .build(); + TestAnalysis analysis = createTestAnalysis(new IndexSettings(indexMetaData, nodeSettings), nodeSettings, new AnalysisBaseformPlugin(nodeSettings), new CommonAnalysisPlugin()); + return analysis; } private void assertSimpleTSOutput(TokenStream stream, String[] expected) throws IOException { diff --git a/src/test/java/org/xbib/elasticsearch/index/analysis/GermanBaseformTokenFilterTests.java b/src/test/java/org/xbib/elasticsearch/index/analysis/GermanBaseformTokenFilterTests.java index 514cfc9..6df15bc 100644 --- a/src/test/java/org/xbib/elasticsearch/index/analysis/GermanBaseformTokenFilterTests.java +++ b/src/test/java/org/xbib/elasticsearch/index/analysis/GermanBaseformTokenFilterTests.java @@ -1,34 +1,28 @@ package org.xbib.elasticsearch.index.analysis; import java.io.IOException; -import java.io.InputStreamReader; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; - +import org.elasticsearch.Version; +import org.elasticsearch.analysis.common.CommonAnalysisPlugin; +import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.index.analysis.AnalysisService; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.analysis.NamedAnalyzer; - +import org.elasticsearch.test.ESTestCase; import org.junit.BeforeClass; import org.junit.Test; +import org.xbib.elasticsearch.plugin.analysis.baseform.AnalysisBaseformPlugin; -import static org.elasticsearch.common.io.Streams.copyToString; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -public class GermanBaseformTokenFilterTests { +public class GermanBaseformTokenFilterTests extends ESTestCase { static NamedAnalyzer analyzer; @BeforeClass public static void create() throws IOException { - Settings settings = Settings.settingsBuilder() - .loadFromSource(copyToStringFromClasspath(("/org/xbib/elasticsearch/index/analysis/baseform_de.json"))) - .build(); - AnalysisService analysisService = MapperTestUtils.analysisService(settings); - analyzer = analysisService.analyzer("baseform"); + TestAnalysis analysis = createTestAnalysis("org/xbib/elasticsearch/index/analysis/baseform_de.json"); + analyzer = analysis.indexAnalyzers.get("baseform"); } @Test @@ -87,8 +81,22 @@ public void test3() throws IOException { assertSimpleTSOutput(analyzer.tokenStream("content", source), expected); } - private static String copyToStringFromClasspath(String path) throws IOException { - return copyToString(new InputStreamReader(EnglishBaseformTokenFilterTests.class.getResourceAsStream(path), "UTF-8")); + private static TestAnalysis createTestAnalysis(String resource) throws IOException { + Settings settings = Settings.builder() + .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) + .loadFromStream(resource, ClassLoader.getSystemClassLoader().getResourceAsStream(resource), false) + .build(); + IndexMetaData indexMetaData = IndexMetaData.builder("test") + .settings(settings) + .numberOfShards(1) + .numberOfReplicas(1) + .build(); + Settings nodeSettings = Settings.builder() + .put(AnalysisBaseformPlugin.SETTING_MAX_CACHE_SIZE.getKey(), 131072) + .put("path.home", System.getProperty("path.home", "/tmp")) + .build(); + TestAnalysis analysis = createTestAnalysis(new IndexSettings(indexMetaData, nodeSettings), nodeSettings, new AnalysisBaseformPlugin(nodeSettings), new CommonAnalysisPlugin()); + return analysis; } private void assertSimpleTSOutput(TokenStream stream, String[] expected) throws IOException { diff --git a/src/test/java/org/xbib/elasticsearch/index/analysis/MapperTestUtils.java b/src/test/java/org/xbib/elasticsearch/index/analysis/MapperTestUtils.java deleted file mode 100644 index bc93967..0000000 --- a/src/test/java/org/xbib/elasticsearch/index/analysis/MapperTestUtils.java +++ /dev/null @@ -1,204 +0,0 @@ -package org.xbib.elasticsearch.index.analysis; - -import org.elasticsearch.Version; -import org.elasticsearch.client.Client; -import org.elasticsearch.cluster.metadata.IndexMetaData; -import org.elasticsearch.common.inject.Injector; -import org.elasticsearch.common.inject.ModulesBuilder; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.settings.SettingsModule; -import org.elasticsearch.env.Environment; -import org.elasticsearch.env.EnvironmentModule; -import org.elasticsearch.index.Index; -import org.elasticsearch.index.IndexNameModule; -import org.elasticsearch.index.analysis.AnalysisModule; -import org.elasticsearch.index.analysis.AnalysisService; -import org.elasticsearch.index.mapper.DocumentMapperParser; -import org.elasticsearch.index.mapper.Mapper; -import org.elasticsearch.index.mapper.MapperService; -import org.elasticsearch.index.mapper.MetadataFieldMapper; -import org.elasticsearch.index.mapper.core.BinaryFieldMapper; -import org.elasticsearch.index.mapper.core.BooleanFieldMapper; -import org.elasticsearch.index.mapper.core.ByteFieldMapper; -import org.elasticsearch.index.mapper.core.CompletionFieldMapper; -import org.elasticsearch.index.mapper.core.DateFieldMapper; -import org.elasticsearch.index.mapper.core.DoubleFieldMapper; -import org.elasticsearch.index.mapper.core.FloatFieldMapper; -import org.elasticsearch.index.mapper.core.IntegerFieldMapper; -import org.elasticsearch.index.mapper.core.LongFieldMapper; -import org.elasticsearch.index.mapper.core.ShortFieldMapper; -import org.elasticsearch.index.mapper.core.StringFieldMapper; -import org.elasticsearch.index.mapper.core.TokenCountFieldMapper; -import org.elasticsearch.index.mapper.core.TypeParsers; -import org.elasticsearch.index.mapper.geo.GeoPointFieldMapper; -import org.elasticsearch.index.mapper.internal.AllFieldMapper; -import org.elasticsearch.index.mapper.internal.IdFieldMapper; -import org.elasticsearch.index.mapper.internal.IndexFieldMapper; -import org.elasticsearch.index.mapper.internal.ParentFieldMapper; -import org.elasticsearch.index.mapper.internal.RoutingFieldMapper; -import org.elasticsearch.index.mapper.internal.SourceFieldMapper; -import org.elasticsearch.index.mapper.internal.TTLFieldMapper; -import org.elasticsearch.index.mapper.internal.TimestampFieldMapper; -import org.elasticsearch.index.mapper.internal.TypeFieldMapper; -import org.elasticsearch.index.mapper.internal.UidFieldMapper; -import org.elasticsearch.index.mapper.internal.VersionFieldMapper; -import org.elasticsearch.index.mapper.ip.IpFieldMapper; -import org.elasticsearch.index.mapper.object.ObjectMapper; -import org.elasticsearch.index.settings.IndexSettingsModule; -import org.elasticsearch.index.similarity.SimilarityLookupService; -import org.elasticsearch.indices.analysis.IndicesAnalysisService; -import org.elasticsearch.indices.mapper.MapperRegistry; -import org.xbib.elasticsearch.plugin.analysis.baseform.AnalysisBaseformPlugin; - -import java.util.LinkedHashMap; -import java.util.Map; - -public class MapperTestUtils { - - public static AnalysisService newAnalysisService(Settings indexSettings) { - Injector parentInjector = new ModulesBuilder().add(new SettingsModule(indexSettings), - new EnvironmentModule(new Environment(indexSettings))).createInjector(); - Index index = new Index("test"); - Injector injector = new ModulesBuilder().add( - new IndexSettingsModule(index, indexSettings), - new IndexNameModule(index), - new AnalysisModule(indexSettings, parentInjector.getInstance(IndicesAnalysisService.class))).createChildInjector(parentInjector); - - return injector.getInstance(AnalysisService.class); - } - - public static SimilarityLookupService newSimilarityLookupService(Settings indexSettings) { - return new SimilarityLookupService(new Index("test"), indexSettings); - } - - public static DocumentMapperParser newDocumentMapperParser() { - return newDocumentMapperParser(Settings.builder() - .put("path.home", System.getProperty("path.home")) - .build()); - } - - public static DocumentMapperParser newDocumentMapperParser(Settings settings) { - Settings forcedSettings = Settings.builder() - .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) - .put(settings) - .build(); - SimilarityLookupService similarityLookupService = newSimilarityLookupService(forcedSettings); - Map mappers = registerBuiltInMappers(); - Map metadataMappers = registerBuiltInMetadataMappers(); - MapperRegistry mapperRegistry = new MapperRegistry(mappers, metadataMappers); - MapperService mapperService = new MapperService(new Index("test"), - forcedSettings, - newAnalysisService(forcedSettings), - similarityLookupService, - null, - mapperRegistry); - return new DocumentMapperParser( - forcedSettings, - mapperService, - newAnalysisService(forcedSettings), - similarityLookupService, - null, - mapperRegistry); - } - - public static MapperService newMapperService(Settings settings, Client client) { - Settings indexSettings = Settings.builder() - .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) - .put("path.home", System.getProperty("path.home")) - .put("client.type", "node") - .put(settings) - .build(); - Index index = new Index("test"); - Injector parentInjector = new ModulesBuilder() - .add(new SettingsModule(indexSettings), - new EnvironmentModule(new Environment(indexSettings))) - .createInjector(); - AnalysisModule analysisModule = new AnalysisModule(indexSettings, - parentInjector.getInstance(IndicesAnalysisService.class)); - new AnalysisBaseformPlugin(settings).onModule(analysisModule); - Injector injector = new ModulesBuilder().add(new IndexSettingsModule(index, indexSettings), - new IndexNameModule(index), - analysisModule) - .createChildInjector(parentInjector); - AnalysisService analysisService = injector.getInstance(AnalysisService.class); - SimilarityLookupService similarityLookupService = new SimilarityLookupService(index, indexSettings); - Map mappers = registerBuiltInMappers(); - Map metadataMappers = registerBuiltInMetadataMappers(); - MapperRegistry mapperRegistry = new MapperRegistry(mappers, metadataMappers); - return new MapperService(new Index("test"), - indexSettings, - analysisService, - similarityLookupService, - null, - mapperRegistry); - } - - public static AnalysisService analysisService() { - Settings settings = Settings.settingsBuilder() - .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) - .put("path.home", System.getProperty("path.home")) - .put("client.type", "node") - .build(); - return newMapperService(settings, null).analysisService(); - } - - public static AnalysisService analysisService(Settings settings) { - Settings newSettings = Settings.settingsBuilder() - .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) - .put("path.home", System.getProperty("path.home")) - .put("client.type", "node") - .put(settings) - .build(); - return newMapperService(newSettings, null).analysisService(); - } - - public static AnalysisService analysisService(String resource) { - Settings settings = Settings.settingsBuilder() - .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) - .put("path.home", System.getProperty("path.home")) - .put("client.type", "node") - .loadFromStream(resource, MapperTestUtils.class.getResourceAsStream(resource)) - .build(); - return newMapperService(settings, null).analysisService(); - } - - // copy from org.elasticsearch.indices.IndicesModule - private static Map registerBuiltInMappers() { - Map mapperParsers = new LinkedHashMap<>(); - mapperParsers.put(ByteFieldMapper.CONTENT_TYPE, new ByteFieldMapper.TypeParser()); - mapperParsers.put(ShortFieldMapper.CONTENT_TYPE, new ShortFieldMapper.TypeParser()); - mapperParsers.put(IntegerFieldMapper.CONTENT_TYPE, new IntegerFieldMapper.TypeParser()); - mapperParsers.put(LongFieldMapper.CONTENT_TYPE, new LongFieldMapper.TypeParser()); - mapperParsers.put(FloatFieldMapper.CONTENT_TYPE, new FloatFieldMapper.TypeParser()); - mapperParsers.put(DoubleFieldMapper.CONTENT_TYPE, new DoubleFieldMapper.TypeParser()); - mapperParsers.put(BooleanFieldMapper.CONTENT_TYPE, new BooleanFieldMapper.TypeParser()); - mapperParsers.put(BinaryFieldMapper.CONTENT_TYPE, new BinaryFieldMapper.TypeParser()); - mapperParsers.put(DateFieldMapper.CONTENT_TYPE, new DateFieldMapper.TypeParser()); - mapperParsers.put(IpFieldMapper.CONTENT_TYPE, new IpFieldMapper.TypeParser()); - mapperParsers.put(StringFieldMapper.CONTENT_TYPE, new StringFieldMapper.TypeParser()); - mapperParsers.put(TokenCountFieldMapper.CONTENT_TYPE, new TokenCountFieldMapper.TypeParser()); - mapperParsers.put(ObjectMapper.CONTENT_TYPE, new ObjectMapper.TypeParser()); - mapperParsers.put(ObjectMapper.NESTED_CONTENT_TYPE, new ObjectMapper.TypeParser()); - mapperParsers.put(TypeParsers.MULTI_FIELD_CONTENT_TYPE, TypeParsers.multiFieldConverterTypeParser); - mapperParsers.put(CompletionFieldMapper.CONTENT_TYPE, new CompletionFieldMapper.TypeParser()); - mapperParsers.put(GeoPointFieldMapper.CONTENT_TYPE, new GeoPointFieldMapper.TypeParser()); - return mapperParsers; - } - - // copy from org.elasticsearch.indices.IndicesModule - private static Map registerBuiltInMetadataMappers() { - Map metadataMapperParsers = new LinkedHashMap<>(); - metadataMapperParsers.put(UidFieldMapper.NAME, new UidFieldMapper.TypeParser()); - metadataMapperParsers.put(IdFieldMapper.NAME, new IdFieldMapper.TypeParser()); - metadataMapperParsers.put(RoutingFieldMapper.NAME, new RoutingFieldMapper.TypeParser()); - metadataMapperParsers.put(IndexFieldMapper.NAME, new IndexFieldMapper.TypeParser()); - metadataMapperParsers.put(SourceFieldMapper.NAME, new SourceFieldMapper.TypeParser()); - metadataMapperParsers.put(TypeFieldMapper.NAME, new TypeFieldMapper.TypeParser()); - metadataMapperParsers.put(AllFieldMapper.NAME, new AllFieldMapper.TypeParser()); - metadataMapperParsers.put(TimestampFieldMapper.NAME, new TimestampFieldMapper.TypeParser()); - metadataMapperParsers.put(TTLFieldMapper.NAME, new TTLFieldMapper.TypeParser()); - metadataMapperParsers.put(VersionFieldMapper.NAME, new VersionFieldMapper.TypeParser()); - metadataMapperParsers.put(ParentFieldMapper.NAME, new ParentFieldMapper.TypeParser()); - return metadataMapperParsers; - } -} diff --git a/src/test/java/org/xbib/elasticsearch/plugin/baseform/BaseformPluginTest.java b/src/test/java/org/xbib/elasticsearch/plugin/baseform/BaseformPluginTest.java deleted file mode 100644 index 584ae84..0000000 --- a/src/test/java/org/xbib/elasticsearch/plugin/baseform/BaseformPluginTest.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.xbib.elasticsearch.plugin.baseform; - -import org.elasticsearch.client.Client; -import org.elasticsearch.common.logging.ESLogger; -import org.elasticsearch.common.logging.ESLoggerFactory; -import org.junit.Test; - -public class BaseformPluginTest extends NodeTestUtils { - - private final static ESLogger logger = ESLoggerFactory.getLogger(BaseformPluginTest.class.getName()); - - @Test - public void test() { - Client client = client("1"); - // TODO - client.close(); - } - -} diff --git a/src/test/java/org/xbib/elasticsearch/plugin/baseform/MockNode.java b/src/test/java/org/xbib/elasticsearch/plugin/baseform/MockNode.java deleted file mode 100644 index 631e9c6..0000000 --- a/src/test/java/org/xbib/elasticsearch/plugin/baseform/MockNode.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.xbib.elasticsearch.plugin.baseform; - -import org.elasticsearch.Version; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.node.Node; -import org.elasticsearch.node.internal.InternalSettingsPreparer; -import org.elasticsearch.plugins.Plugin; - -import java.util.ArrayList; -import java.util.Collection; - -public class MockNode extends Node { - - public MockNode(Settings settings, Collection> classpathPlugins) { - super(InternalSettingsPreparer.prepareEnvironment(settings, null), Version.CURRENT, classpathPlugins); - } - - public MockNode(Settings settings, Class classpathPlugin) { - this(settings, list(classpathPlugin)); - } - - public MockNode(Settings settings) { - this(settings, list()); - } - - private static Collection> list() { - return new ArrayList<>(); - } - - private static Collection> list(Class classpathPlugin) { - Collection> list = new ArrayList<>(); - list.add(classpathPlugin); - return list; - } -} diff --git a/src/test/java/org/xbib/elasticsearch/plugin/baseform/NodeTestUtils.java b/src/test/java/org/xbib/elasticsearch/plugin/baseform/NodeTestUtils.java deleted file mode 100644 index da46c44..0000000 --- a/src/test/java/org/xbib/elasticsearch/plugin/baseform/NodeTestUtils.java +++ /dev/null @@ -1,196 +0,0 @@ -package org.xbib.elasticsearch.plugin.baseform; - -import org.elasticsearch.ElasticsearchTimeoutException; -import org.elasticsearch.action.admin.cluster.health.ClusterHealthAction; -import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest; -import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; -import org.elasticsearch.action.admin.cluster.node.info.NodesInfoRequest; -import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse; -import org.elasticsearch.client.support.AbstractClient; -import org.elasticsearch.cluster.health.ClusterHealthStatus; -import org.elasticsearch.common.logging.ESLogger; -import org.elasticsearch.common.logging.ESLoggerFactory; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.transport.InetSocketTransportAddress; -import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.node.Node; -import org.junit.After; -import org.junit.Before; -import org.xbib.elasticsearch.plugin.analysis.baseform.AnalysisBaseformPlugin; - -import java.io.IOException; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; - -import static org.elasticsearch.common.settings.Settings.settingsBuilder; - -public class NodeTestUtils { - - protected final static ESLogger logger = ESLoggerFactory.getLogger("test"); - - private Map nodes = new HashMap<>(); - - private Map clients = new HashMap<>(); - - private AtomicInteger counter = new AtomicInteger(); - - private String cluster; - - private String host; - - private int port; - - @Before - public void startNodes() { - try { - logger.info("starting"); - setClusterName(); - startNode("1"); - findNodeAddress(); - try { - ClusterHealthResponse healthResponse = client("1").execute(ClusterHealthAction.INSTANCE, - new ClusterHealthRequest().waitForStatus(ClusterHealthStatus.GREEN).timeout(TimeValue.timeValueSeconds(30))).actionGet(); - if (healthResponse != null && healthResponse.isTimedOut()) { - throw new IOException("cluster state is " + healthResponse.getStatus().name() - + ", from here on, everything will fail!"); - } - } catch (ElasticsearchTimeoutException e) { - throw new IOException("timeout, cluster does not respond to health request, cowardly refusing to continue with operations"); - } - } catch (Throwable t) { - logger.error("startNodes failed", t); - } - } - - @After - public void stopNodes() { - try { - closeNodes(); - } catch (Exception e) { - logger.error("can not close nodes", e); - } finally { - try { - deleteFiles(); - logger.info("data files wiped"); - Thread.sleep(2000L); - } catch (IOException e) { - logger.error(e.getMessage(), e); - } catch (InterruptedException e) { - // ignore - } - } - } - - protected void setClusterName() { - this.cluster = "test-analysis-baseform-" - + "-" + System.getProperty("user.name") - + "-" + counter.incrementAndGet(); - } - - protected String getClusterName() { - return cluster; - } - - protected Settings getSettings() { - return settingsBuilder() - .put("host", host) - .put("port", port) - .put("cluster.name", cluster) - .put("path.home", getHome()) - .build(); - } - - protected Settings getNodeSettings() { - return settingsBuilder() - .put("cluster.name", cluster) - .put("cluster.routing.schedule", "50ms") - .put("cluster.routing.allocation.disk.threshold_enabled", false) - .put("discovery.zen.multicast.enabled", true) - .put("discovery.zen.multicast.ping_timeout", "5s") - .put("http.enabled", true) - .put("threadpool.bulk.size", Runtime.getRuntime().availableProcessors()) - .put("threadpool.bulk.queue_size", 16 * Runtime.getRuntime().availableProcessors()) // default is 50, too low - .put("index.number_of_replicas", 0) - .put("path.home", getHome()) - .build(); - } - - protected String getHome() { - return System.getProperty("path.home"); - } - - public void startNode(String id) throws IOException { - buildNode(id).start(); - } - - public AbstractClient client(String id) { - return clients.get(id); - } - - private void closeNodes() throws IOException { - logger.info("closing all clients"); - for (AbstractClient client : clients.values()) { - client.close(); - } - clients.clear(); - logger.info("closing all nodes"); - for (Node node : nodes.values()) { - if (node != null) { - node.close(); - } - } - nodes.clear(); - logger.info("all nodes closed"); - } - - protected void findNodeAddress() { - NodesInfoRequest nodesInfoRequest = new NodesInfoRequest().transport(true); - NodesInfoResponse response = client("1").admin().cluster().nodesInfo(nodesInfoRequest).actionGet(); - Object obj = response.iterator().next().getTransport().getAddress() - .publishAddress(); - if (obj instanceof InetSocketTransportAddress) { - InetSocketTransportAddress address = (InetSocketTransportAddress) obj; - host = address.address().getHostName(); - port = address.address().getPort(); - } - } - - private Node buildNode(String id) throws IOException { - Settings nodeSettings = settingsBuilder() - .put(getNodeSettings()) - .put("name", id) - .build(); - logger.info("settings={}", nodeSettings.getAsMap()); - // ES 2.1 renders NodeBuilder as useless - Node node = new MockNode(nodeSettings, AnalysisBaseformPlugin.class); - AbstractClient client = (AbstractClient)node.client(); - nodes.put(id, node); - clients.put(id, client); - logger.info("clients={}", clients); - return node; - } - - private static void deleteFiles() throws IOException { - Path directory = Paths.get(System.getProperty("path.home") + "/data"); - Files.walkFileTree(directory, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - Files.delete(file); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { - Files.delete(dir); - return FileVisitResult.CONTINUE; - } - }); - } -} From de100f46df302d378644dcce4b931e5d9fce2817 Mon Sep 17 00:00:00 2001 From: Jan Faber Date: Mon, 9 Apr 2018 10:24:50 +0200 Subject: [PATCH 02/13] move properties to gradle.properties --- build.gradle | 6 +- config/checkstyle/checkstyle.xml | 323 ++++++++++++++++++ gradle.properties | 6 +- gradle/ext.gradle | 9 - .../templates/plugin-descriptor.properties | 3 - 5 files changed, 331 insertions(+), 16 deletions(-) create mode 100644 config/checkstyle/checkstyle.xml diff --git a/build.gradle b/build.gradle index d187709..5caa744 100644 --- a/build.gradle +++ b/build.gradle @@ -89,9 +89,9 @@ task makePluginDescriptor(type: Copy) { into 'build/tmp/plugin' expand([ 'descriptor': [ - 'name': pluginName, - 'classname': pluginClassname, - 'description': pluginDescription, + 'name': project.property('pluginName'), + 'classname': project.property('pluginClassname'), + 'description': project.property('pluginDescription'), 'version': project.property('version'), 'javaVersion': project.property('targetCompatibility'), 'elasticsearchVersion' : project.property('elasticsearch.version') diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..52fe33c --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,323 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle.properties b/gradle.properties index 1d34262..c0b413a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,10 @@ +pluginName = baseform +pluginClassname = org.xbib.elasticsearch.plugin.analysis.baseform.AnalysisBaseformPlugin +pluginDescription = Baseform plugin for Elasticsearch + group = org.xbib.elasticsearch.plugin name = elasticsearch-analysis-baseform -version = 6.1.1.0 +version = 6.1.1.1 elasticsearch.version = 6.1.1 log4j.version = 2.9.1 diff --git a/gradle/ext.gradle b/gradle/ext.gradle index b768e90..0c623b9 100644 --- a/gradle/ext.gradle +++ b/gradle/ext.gradle @@ -1,16 +1,7 @@ ext { - pluginName = 'decompound' - pluginClassname = 'org.xbib.elasticsearch.plugin.analysis.decompound.AnalysisDecompoundPlugin' - pluginDescription = 'Decompounder plugin for Elasticsearch' user = 'jprante' - name = 'elasticsearch-analysis-decompound' scmUrl = 'https://github.com/' + user + '/' + name scmConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git' scmDeveloperConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git' - versions = [ - 'elasticsearch' : '5.1.1', - 'log4j': '2.5', - 'junit' : '4.12' - ] } diff --git a/src/main/templates/plugin-descriptor.properties b/src/main/templates/plugin-descriptor.properties index d599e0c..0b9867c 100644 --- a/src/main/templates/plugin-descriptor.properties +++ b/src/main/templates/plugin-descriptor.properties @@ -1,9 +1,6 @@ classname=${descriptor.classname} name=${descriptor.name} description=${descriptor.description} -jvm=${descriptor.jvm} -site=${descriptor.site} -isolated=${descriptor.isolated} version=${descriptor.version} java.version=${descriptor.javaVersion} elasticsearch.version=${descriptor.elasticsearchVersion} From e61cacb327baf72317de163b55350c56ad6afac7 Mon Sep 17 00:00:00 2001 From: mos Date: Fri, 13 Jul 2018 14:57:07 +0200 Subject: [PATCH 03/13] Update to Elasticsearch Version 6.3.1 --- build.gradle | 2 +- gradle.properties | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 5caa744..02573d6 100644 --- a/build.gradle +++ b/build.gradle @@ -102,7 +102,7 @@ task makePluginDescriptor(type: Copy) { task buildPluginZip(type: Zip, dependsOn: [':jar', ':makePluginDescriptor']) { from configurations.distJars from 'build/tmp/plugin' - into 'elasticsearch' + //into 'elasticsearch' classifier = 'plugin' } diff --git a/gradle.properties b/gradle.properties index c0b413a..8f2b4fa 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,9 +4,9 @@ pluginDescription = Baseform plugin for Elasticsearch group = org.xbib.elasticsearch.plugin name = elasticsearch-analysis-baseform -version = 6.1.1.1 +version = 6.3.1-00 -elasticsearch.version = 6.1.1 +elasticsearch.version = 6.3.1 log4j.version = 2.9.1 junit.version = 4.12 wagon.version = 2.12 From 9f12aec0ee5582a978c710dec2cabc34c437e11f Mon Sep 17 00:00:00 2001 From: mos Date: Fri, 21 Sep 2018 17:33:14 +0200 Subject: [PATCH 04/13] Upgrade plugin to Elasticsearch Version 6.4.1 (SUREMO-170) --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 8f2b4fa..1e7637b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,9 +4,9 @@ pluginDescription = Baseform plugin for Elasticsearch group = org.xbib.elasticsearch.plugin name = elasticsearch-analysis-baseform -version = 6.3.1-00 +version = 6.4.1-00 -elasticsearch.version = 6.3.1 +elasticsearch.version = 6.4.1 log4j.version = 2.9.1 junit.version = 4.12 wagon.version = 2.12 From 09ea690006455d6f4b07169317e79cb569db29ea Mon Sep 17 00:00:00 2001 From: mos Date: Fri, 28 Sep 2018 17:08:31 +0200 Subject: [PATCH 05/13] add documentation --- README.md | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0268123..214eeb1 100644 --- a/README.md +++ b/README.md @@ -61,11 +61,14 @@ In the settings, set up a token filter of type "baseform" and language "de":: } By using such a tokenizer, the sentence -"Die Jahresfeier der Rechtsanwaltskanzleien auf dem Donaudampfschiff hat viel Ökosteuer gekostet" + + "Die Jahresfeier der Rechtsanwaltskanzleien auf dem Donaudampfschiff hat viel Ökosteuer gekostet" + will be tokenized into -"Die", "Die", "Jahresfeier", "Jahresfeier", "der", "der", "Rechtsanwaltskanzleien", "Rechtsanwaltskanzlei", -"auf", "auf", "dem", "der", "Donaudampfschiff", "Donaudampfschiff", "hat", "haben", "viel", "viel", -"Ökosteuer", "Ökosteuer", "gekostet", "kosten" + + "Die", "Die", "Jahresfeier", "Jahresfeier", "der", "der", "Rechtsanwaltskanzleien", "Rechtsanwaltskanzlei", + "auf", "auf", "dem", "der", "Donaudampfschiff", "Donaudampfschiff", "hat", "haben", "viel", "viel", + "Ökosteuer", "Ökosteuer", "gekostet", "kosten" It is recommended to add the [Unique token filter](http://www.elasticsearch.org/guide/reference/index-modules/analysis/unique-tokenfilter.html) to skip tokens that occur more than once. @@ -115,6 +118,18 @@ this token stream will be produced:: As an alternative, separate dictionaries for `en-verbs` and `en-nouns` are available. +## Caching + +The time consumed by the baseform computation may increase your overall indexing time drastically if applied in the billions. You can configure the cache size (in number of entries) for mapping a token to an array of baseform tokens. +Reaching the cache size limit results in clearing of the cache and starting anew. This setting and the cache respectively is applied to a node, so configure it in the elasticsearch.yml file: + +``` +# default: 8388608 entries +# minimum: 131072 entries +# baseform_max_cache_size: 8388608 +``` + + # License Elasticsearch Baseform Analysis Plugin @@ -148,3 +163,5 @@ and is distributed under CC-BY-SA http://creativecommons.org/licenses/by-sa/3.0/ The english baseforms are a modified version of the english.dict file of http://languagetool.org/download/snapshots/LanguageTool-20131115-snapshot.zip which is licensed under LGPL http://www.fsf.org/licensing/licenses/lgpl.html#SEC1 + +GBI-Genios Deutsche Wirtschaftsdatenbank GmbH for adding the caching-functionality. From 8d77b82208ab5ad233ab05aa347216364831b604 Mon Sep 17 00:00:00 2001 From: mos Date: Fri, 28 Sep 2018 17:18:09 +0200 Subject: [PATCH 06/13] add documentation --- README.md | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/README.md b/README.md index 214eeb1..00b429d 100644 --- a/README.md +++ b/README.md @@ -8,26 +8,9 @@ Currently, only baseforms for german and english are implemented. Example: the german base form of `zurückgezogen` is `zurückziehen`. -## Versions - -| Plugin | Elasticsearch | Release date | -| --------- | --------------- | -------------| -| 2.2.1.1 | 2.2.1 | Jun 22, 2016 | -| 2.2.1.0 | 2.2.1 | Apr 23, 2016 | -| 1.4.0.0 | 1.4.0 | Feb 19, 2015 | -| 1.3.0.0 | 1.3.1 | Jul 30, 2014 | - ## Installation -### Elasticsearch 2.x - - ./bin/plugin install http://xbib.org/repository/org/xbib/elasticsearch/plugin/elasticsearch-analysis-baseform/2.2.1.1/elasticsearch-analysis-baseform-2.2.1.1-plugin.zip - -### Elasticsearch 1.x - - ./bin/plugin -install analysis-baseform -url http://xbib.org/repository/org/xbib/elasticsearch/plugin/elasticsearch-analysis-baseform/1.4.0.0/elasticsearch-analysis-baseform-1.4.0.0-plugin.zip - -Do not forget to restart the node after installing. +Use Gradle to build the plugin and install it using the elasticsearch-plugin command. Check the "gradle.properties" for the supported version. ## Project docs From 9e95f5e3adb0e3caefd6d19b6940b65f8b17a51b Mon Sep 17 00:00:00 2001 From: mos Date: Fri, 14 Dec 2018 10:23:59 +0100 Subject: [PATCH 07/13] Update to ES 6.5.3 --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 1e7637b..a92c6af 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,9 +4,9 @@ pluginDescription = Baseform plugin for Elasticsearch group = org.xbib.elasticsearch.plugin name = elasticsearch-analysis-baseform -version = 6.4.1-00 +version = 6.5.3-00 -elasticsearch.version = 6.4.1 +elasticsearch.version = 6.5.3 log4j.version = 2.9.1 junit.version = 4.12 wagon.version = 2.12 From 3a1f7655ccfe57e853acc29b47f031efa61277a9 Mon Sep 17 00:00:00 2001 From: mos Date: Fri, 1 Mar 2019 09:31:32 +0100 Subject: [PATCH 08/13] Update to ES 6.6.1 --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index a92c6af..6fa3969 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,9 +4,9 @@ pluginDescription = Baseform plugin for Elasticsearch group = org.xbib.elasticsearch.plugin name = elasticsearch-analysis-baseform -version = 6.5.3-00 +version = 6.6.1-00 -elasticsearch.version = 6.5.3 +elasticsearch.version = 6.6.1 log4j.version = 2.9.1 junit.version = 4.12 wagon.version = 2.12 From 27ba7f1be21f55ff4f8626267b76c84e1773adf5 Mon Sep 17 00:00:00 2001 From: Jan Faber Date: Wed, 29 May 2019 10:10:33 +0200 Subject: [PATCH 09/13] add payload if necessary; non originals always win --- .../baseform/BaseformTokenFilter.java | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/xbib/elasticsearch/index/analysis/baseform/BaseformTokenFilter.java b/src/main/java/org/xbib/elasticsearch/index/analysis/baseform/BaseformTokenFilter.java index f1a7f4f..2fb209c 100644 --- a/src/main/java/org/xbib/elasticsearch/index/analysis/baseform/BaseformTokenFilter.java +++ b/src/main/java/org/xbib/elasticsearch/index/analysis/baseform/BaseformTokenFilter.java @@ -13,14 +13,19 @@ import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; import org.apache.lucene.analysis.tokenattributes.PackedTokenAttributeImpl; +import org.apache.lucene.analysis.tokenattributes.PayloadAttribute; import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute; import org.apache.lucene.util.AttributeSource; +import org.apache.lucene.util.BytesRef; import org.xbib.elasticsearch.common.fsa.Dictionary; public class BaseformTokenFilter extends TokenFilter { private static final Logger LOG = LogManager.getLogger(BaseformTokenFilter.class); + private static final byte ORIGINAL_TYPE = 1; + private static final byte BASEFORM_TYPE = 4; + private static ConcurrentHashMap TERM_CACHE; private static final AtomicLong termCacheCount = new AtomicLong(0); @@ -36,6 +41,8 @@ public class BaseformTokenFilter extends TokenFilter { private final Dictionary dictionary; private final CharTermAttribute termAtt = addAttribute(CharTermAttribute.class); + + private final PayloadAttribute payloadAtt = addAttribute(PayloadAttribute.class); private final PositionIncrementAttribute posIncAtt = addAttribute(PositionIncrementAttribute.class); @@ -58,6 +65,7 @@ public final boolean incrementToken() throws IOException { restoreState(current); termAtt.setEmpty().append(token); posIncAtt.setPositionIncrement(0); + setPayload(BASEFORM_TYPE); return true; } if (input.incrementToken()) { @@ -65,6 +73,7 @@ public final boolean incrementToken() throws IOException { if (!tokens.isEmpty()) { current = captureState(); } + setPayload(ORIGINAL_TYPE); return true; } else { return false; @@ -83,7 +92,7 @@ protected void baseform() throws CharacterCodingException { try { CharSequence baseform = dictionary.lookup(t); - if (baseform == null) { + if (baseform == null || term.equals(baseform)) { return NO_TERMS; } return baseform; @@ -97,7 +106,24 @@ protected void baseform() throws CharacterCodingException { tokens.add(impl); } } - + + private void setPayload(byte tokenType) { + BytesRef payload = payloadAtt.getPayload(); + if (tokenType == ORIGINAL_TYPE) { + if (payload == null) { + payload = new BytesRef(); + } + } else { + if (payload != null && payload.length > 0) { + payload = BytesRef.deepCopyOf(payload); + } else { + payload = new BytesRef(new byte[1]); + } + payload.bytes[payload.offset] |= tokenType; + } + payloadAtt.setPayload(payload); + } + private void checkCacheSize() { needsClearCache.set(false); final Runtime runtime = Runtime.getRuntime(); From 215c5dae9845a1b2817880ed401416bf7d034081 Mon Sep 17 00:00:00 2001 From: Jan Faber Date: Wed, 29 May 2019 12:42:19 +0200 Subject: [PATCH 10/13] upgrade gradle wrapper version for newer jdks --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f16d266..c30d4c0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.7-bin.zip From c162570290faa3f730080a78a03e19ef1f025655 Mon Sep 17 00:00:00 2001 From: mos Date: Tue, 4 Jun 2019 15:59:55 +0200 Subject: [PATCH 11/13] Version-Update; Exact-Phrase for Baseform (SUREMO-456) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 6fa3969..4b4c1af 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ pluginDescription = Baseform plugin for Elasticsearch group = org.xbib.elasticsearch.plugin name = elasticsearch-analysis-baseform -version = 6.6.1-00 +version = 6.6.1-01 elasticsearch.version = 6.6.1 log4j.version = 2.9.1 From ed348a21400573c97078b8849f0796e6289b49ef Mon Sep 17 00:00:00 2001 From: Jan Faber Date: Wed, 5 Jun 2019 13:43:43 +0200 Subject: [PATCH 12/13] adapt test cases for duplicate token filtering --- .../index/analysis/BaseformTokenFilterTests.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/test/java/org/xbib/elasticsearch/index/analysis/BaseformTokenFilterTests.java b/src/test/java/org/xbib/elasticsearch/index/analysis/BaseformTokenFilterTests.java index b3eb26c..14a1916 100644 --- a/src/test/java/org/xbib/elasticsearch/index/analysis/BaseformTokenFilterTests.java +++ b/src/test/java/org/xbib/elasticsearch/index/analysis/BaseformTokenFilterTests.java @@ -24,24 +24,17 @@ public void testOne() throws IOException { String[] expected = { "Die", - "Die", - "Jahresfeier", "Jahresfeier", "der", - "der", "Rechtsanwaltskanzleien", "Rechtsanwaltskanzlei", "auf", - "auf", "dem", "der", "Donaudampfschiff", - "Donaudampfschiff", "hat", "haben", "viel", - "viel", - "Ökosteuer", "Ökosteuer", "gekostet", "kosten" @@ -60,8 +53,6 @@ public void testTwo() throws IOException { String[] expected = { "Das", - "Das", - "sind", "sind", "Autos", "Auto", @@ -69,7 +60,6 @@ public void testTwo() throws IOException { "der", "Nudeln", "Nudel", - "transportieren", "transportieren" }; TestAnalysis analysis = createTestAnalysis(); @@ -89,8 +79,6 @@ public void testThree() throws IOException { "wurde", "werden", "zum", - "zum", - "tollen", "tollen", "gemacht", "machen" From 3c342d6a52c1a0f5814cbf7181aa889d65dcaaa5 Mon Sep 17 00:00:00 2001 From: mos Date: Fri, 7 Jun 2019 10:43:31 +0200 Subject: [PATCH 13/13] version-Update; Exact-Phrase for Baseform finished(SUREMO-456) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 4b4c1af..b099adb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ pluginDescription = Baseform plugin for Elasticsearch group = org.xbib.elasticsearch.plugin name = elasticsearch-analysis-baseform -version = 6.6.1-01 +version = 6.6.1-02 elasticsearch.version = 6.6.1 log4j.version = 2.9.1