/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch 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.elasticsearch.ingest;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.elasticsearch.Version;
import org.elasticsearch.cluster.metadata.IndexMetaData;
import org.elasticsearch.common.UUIDs;
import org.elasticsearch.common.component.AbstractLifecycleComponent;
import org.elasticsearch.common.lease.Releasable;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.index.IndexSettings;
import org.elasticsearch.index.analysis.AnalysisRegistry;
import org.elasticsearch.index.analysis.AnalysisService;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Service for maintaining currently defined custom analyzers for the analyzer ingest processor
*/
public class IngestAnalysisService extends AbstractLifecycleComponent {
private final Setting<Settings> ingestAnalysisGroupSetting;
// injected later on during plugin initialization
private AnalysisRegistry analysisRegistry;
// updated dynamically on settings change
private AnalysisServiceHolder analysisServiceHolder;
private final Object holderLock = new Object();
public IngestAnalysisService(Settings settings) {
super(settings);
ingestAnalysisGroupSetting = Setting.groupSetting("ingest.analysis.", this::validateSettings, Setting.Property.Dynamic,
Setting.Property.NodeScope);
}
public Setting<Settings> getIngestAnalysisGroupSetting() {
return ingestAnalysisGroupSetting;
}
public void setAnalysisRegistry(AnalysisRegistry analysisRegistry) {
assert this.analysisRegistry == null && analysisRegistry != null; // shouldn't initialize more then once
this.analysisRegistry = analysisRegistry;
}
public void validateSettings(Settings analysis) {
// During node startup the analysisRegistry registry is null
// That's fine - we will re-validate the settings a bit later when we build the initial analysis service anyway
if (analysisRegistry != null && analysis.isEmpty() == false) {
// We only need to test in case of custom settings
buildAnalysisService(analysis).close();
}
}
/**
* Replaces the internal analysis service with the new version.
* This method is called during initialization and when analysis settings are dynamically changed
*/
public void setAnalysisSettings(Settings analysis) {
setAnalysisServiceHolder(new AnalysisServiceHolder(buildAnalysisService(analysis)));
}
/**
* Builds an analysis service based on the specified analysis settings
*/
private AnalysisService buildAnalysisService(Settings analysis) {
assert analysisRegistry != null;
Settings settings = getAnonymousSettings(Settings.builder().put(analysis).normalizePrefix("index.analysis.").build());
IndexSettings indexSettings = getNaIndexSettings(settings);
try {
return analysisRegistry.build(indexSettings);
} catch (Exception ex) {
throw new IllegalArgumentException("Couldn't parse ingest analyzer definition [" + analysis.toDelimitedString(',') + "]", ex);
}
}
/**
* Sets the new analysis holder in a thread-safe manner
*/
private void setAnalysisServiceHolder(AnalysisServiceHolder analysisServiceHolder) {
synchronized (holderLock) {
AnalysisServiceHolder holder = this.analysisServiceHolder;
this.analysisServiceHolder = analysisServiceHolder;
if (holder != null) {
holder.close();
}
}
}
/**
* Returns the new analysis holder for use. The acquired service holder should be released by calling release() method when
* operations with the wrapped analyzer is completed.
*/
public AnalysisServiceHolder acquireAnalysisServiceHolder() {
synchronized (holderLock) {
if (analysisServiceHolder == null) {
throw new IllegalArgumentException("cannot acquire analysis service");
}
analysisServiceHolder.acquire();
return analysisServiceHolder;
}
}
@Override
protected void doStart() {
}
@Override
protected void doStop() {
}
@Override
protected void doClose() {
if (analysisServiceHolder != null) {
analysisServiceHolder.close();
analysisServiceHolder = null;
}
}
/**
* Reference counting analysis service holder
*/
public static class AnalysisServiceHolder implements Releasable {
private final AtomicInteger userCount = new AtomicInteger(1);
private final AnalysisService analysisService;
public AnalysisServiceHolder(AnalysisService analysisService) {
this.analysisService = analysisService;
}
public void acquire() {
userCount.incrementAndGet();
}
public void release() {
if (userCount.decrementAndGet() == 0) {
analysisService.close();
}
}
public boolean hasAnalyzer(String analyzerName) {
return analysisService.analyzer(analyzerName) != null;
}
public TokenStream tokenStream(String analyzerName, String fieldName, String text) {
Analyzer analyzer = analysisService.analyzer(analyzerName);
if (analyzer == null) {
throw new IllegalArgumentException("unknown analyzer [" + analyzerName + "]");
}
return analyzer.tokenStream(fieldName, text);
}
@Override
public void close() {
release();
}
}
private static IndexSettings getNaIndexSettings(Settings settings) {
IndexMetaData metaData = IndexMetaData.builder(IndexMetaData.INDEX_UUID_NA_VALUE).settings(settings).build();
return new IndexSettings(metaData, Settings.EMPTY);
}
private static Settings getAnonymousSettings(Settings providerSetting) {
return Settings.builder().put(providerSetting)
// for _na_
.put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)
.put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0)
.put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1)
.put(IndexMetaData.SETTING_INDEX_UUID, UUIDs.randomBase64UUID())
.build();
}
}