<commit_before>/*
 * Licensed to ElasticSearch and <NAME> 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.search.suggest;

import com.google.common.base.Charsets;
import com.google.common.io.Resources;
import org.apache.lucene.util.LuceneTestCase.Slow;
import org.elasticsearch.ElasticSearchException;
import org.elasticsearch.ElasticSearchIllegalStateException;
import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder;
import org.elasticsearch.action.search.*;
import org.elasticsearch.client.Client;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.search.suggest.SuggestBuilder.SuggestionBuilder;
import org.elasticsearch.search.suggest.phrase.PhraseSuggestionBuilder;
import org.elasticsearch.search.suggest.phrase.PhraseSuggestionBuilder.DirectCandidateGenerator;
import org.elasticsearch.search.suggest.term.TermSuggestionBuilder;
import org.elasticsearch.test.ElasticsearchIntegrationTest;
import org.elasticsearch.test.hamcrest.ElasticsearchAssertions;
import org.junit.Test;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.ExecutionException;

import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_REPLICAS;
import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_SHARDS;
import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilder;
import static org.elasticsearch.index.query.QueryBuilders.matchQuery;
import static org.elasticsearch.search.suggest.SuggestBuilder.phraseSuggestion;
import static org.elasticsearch.search.suggest.SuggestBuilder.termSuggestion;
import static org.elasticsearch.search.suggest.phrase.PhraseSuggestionBuilder.candidateGenerator;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.*;
import static org.hamcrest.Matchers.*;

/**
 * Integration tests for term and phrase suggestions.  Many of these tests many requests that vary only slightly from one another.  Where
 * possible these tests should declare for the first request, make the request, modify the configuration for the next request, make that
 * request, modify again, request again, etc.  This makes it very obvious what changes between requests.
 */
public class SuggestSearchTests extends ElasticsearchIntegrationTest {


    @Test // see #3196
    public void testSuggestAcrossMultipleIndices() throws IOException {
        prepareCreate("test").setSettings(
                SETTING_NUMBER_OF_SHARDS, between(1, 5),
                SETTING_NUMBER_OF_REPLICAS, between(0, 1)).get();
        ensureGreen();

        index("test", "type1", "1", "text", "abcd");
        index("test", "type1", "2", "text", "aacd");
        index("test", "type1", "3", "text", "abbd");
        index("test", "type1", "4", "text", "abcc");
        refresh();

        TermSuggestionBuilder termSuggest = termSuggestion("test")
                .suggestMode("always") // Always, otherwise the results can vary between requests.
                .text("abcd")
                .field("text");
        logger.info("--> run suggestions with one index");
        searchSuggest(client(), termSuggest);
        prepareCreate("test_1").setSettings(
                SETTING_NUMBER_OF_SHARDS, between(1, 5),
                SETTING_NUMBER_OF_REPLICAS, between(0, 1)).get();
        ensureGreen();

        index("test_1", "type1", "1", "text", "ab cd");
        index("test_1", "type1", "2", "text", "aa cd");
        index("test_1", "type1", "3", "text", "ab bd");
        index("test_1", "type1", "4", "text", "ab cc");
        refresh();
        termSuggest = termSuggestion("test")
                .suggestMode("always") // Always, otherwise the results can vary between requests.
                .text("ab cd")
                .minWordLength(1)
                .field("text");
        logger.info("--> run suggestions with two indices");
        searchSuggest(client(), termSuggest);


        XContentBuilder mapping = XContentFactory.jsonBuilder().startObject().startObject("type1")
                .startObject("properties")
                .startObject("text").field("type", "string").field("analyzer", "keyword").endObject()
                .endObject()
                .endObject().endObject();
        assertAcked(prepareCreate("test_2").setSettings(settingsBuilder()
                .put(SETTING_NUMBER_OF_SHARDS, between(1, 5))
                .put(SETTING_NUMBER_OF_REPLICAS, between(0, 1))
                ).addMapping("type1", mapping));
        ensureGreen();

        index("test_2", "type1", "1", "text", "ab cd");
        index("test_2", "type1", "2", "text", "aa cd");
        index("test_2", "type1", "3", "text", "ab bd");
        index("test_2", "type1", "4", "text", "ab cc");
        index("test_2", "type1", "1", "text", "abcd");
        index("test_2", "type1", "2", "text", "aacd");
        index("test_2", "type1", "3", "text", "abbd");
        index("test_2", "type1", "4", "text", "abcc");
        refresh();

        termSuggest = termSuggestion("test")
                .suggestMode("always") // Always, otherwise the results can vary between requests.
                .text("ab cd")
                .minWordLength(1)
                .field("text");
        logger.info("--> run suggestions with three indices");
        try {
            searchSuggest(client(), termSuggest);
            fail(" can not suggest across multiple indices with different analysis chains");
        } catch (ReduceSearchPhaseException ex) {
            assertThat(ex.getCause(), instanceOf(ElasticSearchIllegalStateException.class));
            assertThat(ex.getCause().getMessage(), endsWith("Suggest entries have different sizes actual [1] expected [2]"));
        } catch (ElasticSearchIllegalStateException ex) {
            assertThat(ex.getMessage(), endsWith("Suggest entries have different sizes actual [1] expected [2]"));
        }


        termSuggest = termSuggestion("test")
                .suggestMode("always") // Always, otherwise the results can vary between requests.
                .text("ABCD")
                .minWordLength(1)
                .field("text");
        logger.info("--> run suggestions with four indices");
        try {
            searchSuggest(client(), termSuggest);
            fail(" can not suggest across multiple indices with different analysis chains");
        } catch (ReduceSearchPhaseException ex) {
            assertThat(ex.getCause(), instanceOf(ElasticSearchIllegalStateException.class));
            assertThat(ex.getCause().getMessage(), endsWith("Suggest entries have different text actual [ABCD] expected [abcd]"));
        } catch (ElasticSearchIllegalStateException ex) {
            assertThat(ex.getMessage(), endsWith("Suggest entries have different text actual [ABCD] expected [abcd]"));
        }


    }

    @Test // see #3037
    public void testSuggestModes() throws IOException {
        CreateIndexRequestBuilder builder = prepareCreate("test").setSettings(settingsBuilder()
                .put(SETTING_NUMBER_OF_SHARDS, 1)
                .put(SETTING_NUMBER_OF_REPLICAS, 0)
                .put("index.analysis.analyzer.biword.tokenizer", "standard")
                .putArray("index.analysis.analyzer.biword.filter", "shingler", "lowercase")
                .put("index.analysis.filter.shingler.type", "shingle")
                .put("index.analysis.filter.shingler.min_shingle_size", 2)
                .put("index.analysis.filter.shingler.max_shingle_size", 3));
        
        XContentBuilder mapping = XContentFactory.jsonBuilder().startObject().startObject("type1")
                .startObject("properties")
                .startObject("name")
                    .field("type", "multi_field")
                    .field("path", "just_name")
                    .startObject("fields")
                        .startObject("name")
                            .field("type", "string")
                        .endObject()
                        .startObject("name_shingled")
                            .field("type", "string")
                            .field("index_analyzer", "biword")
                            .field("search_analyzer", "standard")
                        .endObject()
                    .endObject()<commit_msg>Fix SuggestSearchTests to expect any order in the error message<commit_after>/*
 * Licensed to ElasticSearch and <NAME> 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.search.suggest;

import com.google.common.base.Charsets;
import com.google.common.io.Resources;
import org.apache.lucene.util.LuceneTestCase.Slow;
import org.elasticsearch.ElasticSearchException;
import org.elasticsearch.ElasticSearchIllegalStateException;
import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder;
import org.elasticsearch.action.search.*;
import org.elasticsearch.client.Client;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.search.suggest.SuggestBuilder.SuggestionBuilder;
import org.elasticsearch.search.suggest.phrase.PhraseSuggestionBuilder;
import org.elasticsearch.search.suggest.phrase.PhraseSuggestionBuilder.DirectCandidateGenerator;
import org.elasticsearch.search.suggest.term.TermSuggestionBuilder;
import org.elasticsearch.test.ElasticsearchIntegrationTest;
import org.elasticsearch.test.hamcrest.ElasticsearchAssertions;
import org.junit.Test;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.ExecutionException;

import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_REPLICAS;
import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_SHARDS;
import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilder;
import static org.elasticsearch.index.query.QueryBuilders.matchQuery;
import static org.elasticsearch.search.suggest.SuggestBuilder.phraseSuggestion;
import static org.elasticsearch.search.suggest.SuggestBuilder.termSuggestion;
import static org.elasticsearch.search.suggest.phrase.PhraseSuggestionBuilder.candidateGenerator;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.*;
import static org.hamcrest.Matchers.*;

/**
 * Integration tests for term and phrase suggestions.  Many of these tests many requests that vary only slightly from one another.  Where
 * possible these tests should declare for the first request, make the request, modify the configuration for the next request, make that
 * request, modify again, request again, etc.  This makes it very obvious what changes between requests.
 */
public class SuggestSearchTests extends ElasticsearchIntegrationTest {


    @Test // see #3196
    public void testSuggestAcrossMultipleIndices() throws IOException {
        prepareCreate("test").setSettings(
                SETTING_NUMBER_OF_SHARDS, between(1, 5),
                SETTING_NUMBER_OF_REPLICAS, between(0, 1)).get();
        ensureGreen();

        index("test", "type1", "1", "text", "abcd");
        index("test", "type1", "2", "text", "aacd");
        index("test", "type1", "3", "text", "abbd");
        index("test", "type1", "4", "text", "abcc");
        refresh();

        TermSuggestionBuilder termSuggest = termSuggestion("test")
                .suggestMode("always") // Always, otherwise the results can vary between requests.
                .text("abcd")
                .field("text");
        logger.info("--> run suggestions with one index");
        searchSuggest(client(), termSuggest);
        prepareCreate("test_1").setSettings(
                SETTING_NUMBER_OF_SHARDS, between(1, 5),
                SETTING_NUMBER_OF_REPLICAS, between(0, 1)).get();
        ensureGreen();

        index("test_1", "type1", "1", "text", "ab cd");
        index("test_1", "type1", "2", "text", "aa cd");
        index("test_1", "type1", "3", "text", "ab bd");
        index("test_1", "type1", "4", "text", "ab cc");
        refresh();
        termSuggest = termSuggestion("test")
                .suggestMode("always") // Always, otherwise the results can vary between requests.
                .text("ab cd")
                .minWordLength(1)
                .field("text");
        logger.info("--> run suggestions with two indices");
        searchSuggest(client(), termSuggest);


        XContentBuilder mapping = XContentFactory.jsonBuilder().startObject().startObject("type1")
                .startObject("properties")
                .startObject("text").field("type", "string").field("analyzer", "keyword").endObject()
                .endObject()
                .endObject().endObject();
        assertAcked(prepareCreate("test_2").setSettings(settingsBuilder()
                .put(SETTING_NUMBER_OF_SHARDS, between(1, 5))
                .put(SETTING_NUMBER_OF_REPLICAS, between(0, 1))
                ).addMapping("type1", mapping));
        ensureGreen();

        index("test_2", "type1", "1", "text", "ab cd");
        index("test_2", "type1", "2", "text", "aa cd");
        index("test_2", "type1", "3", "text", "ab bd");
        index("test_2", "type1", "4", "text", "ab cc");
        index("test_2", "type1", "1", "text", "abcd");
        index("test_2", "type1", "2", "text", "aacd");
        index("test_2", "type1", "3", "text", "abbd");
        index("test_2", "type1", "4", "text", "abcc");
        refresh();

        termSuggest = termSuggestion("test")
                .suggestMode("always") // Always, otherwise the results can vary between requests.
                .text("ab cd")
                .minWordLength(1)
                .field("text");
        logger.info("--> run suggestions with three indices");
        try {
            searchSuggest(client(), termSuggest);
            fail(" can not suggest across multiple indices with different analysis chains");
        } catch (ReduceSearchPhaseException ex) {
            assertThat(ex.getCause(), instanceOf(ElasticSearchIllegalStateException.class));
            assertThat(ex.getCause().getMessage(),
                    anyOf(endsWith("Suggest entries have different sizes actual [1] expected [2]"),
                            endsWith("Suggest entries have different sizes actual [2] expected [1]")));
        } catch (ElasticSearchIllegalStateException ex) {
            assertThat(ex.getMessage(), anyOf(endsWith("Suggest entries have different sizes actual [1] expected [2]"),
                    endsWith("Suggest entries have different sizes actual [2] expected [1]")));
        }


        termSuggest = termSuggestion("test")
                .suggestMode("always") // Always, otherwise the results can vary between requests.
                .text("ABCD")
                .minWordLength(1)
                .field("text");
        logger.info("--> run suggestions with four indices");
        try {
            searchSuggest(client(), termSuggest);
            fail(" can not suggest across multiple indices with different analysis chains");
        } catch (ReduceSearchPhaseException ex) {
            assertThat(ex.getCause(), instanceOf(ElasticSearchIllegalStateException.class));
            assertThat(ex.getCause().getMessage(), anyOf(endsWith("Suggest entries have different text actual [ABCD] expected [abcd]"),
                    endsWith("Suggest entries have different text actual [abcd] expected [ABCD]")));
        } catch (ElasticSearchIllegalStateException ex) {
            assertThat(ex.getMessage(), anyOf(endsWith("Suggest entries have different text actual [ABCD] expected [abcd]"),
                    endsWith("Suggest entries have different text actual [abcd] expected [ABCD]")));
        }


    }

    @Test // see #3037
    public void testSuggestModes() throws IOException {
        CreateIndexRequestBuilder builder = prepareCreate("test").setSettings(settingsBuilder()
                .put(SETTING_NUMBER_OF_SHARDS, 1)
                .put(SETTING_NUMBER_OF_REPLICAS, 0)
                .put("index.analysis.analyzer.biword.tokenizer", "standard")
                .putArray("index.analysis.analyzer.biword.filter", "shingler", "lowercase")
                .put("index.analysis.filter.shingler.type", "shingle")
                .put("index.analysis.filter.shingler.min_shingle_size", 2)
                .put("index.analysis.filter.shingler.max_shingle_size", 3));
        
        XContentBuilder mapping = XContentFactory.jsonBuilder().startObject().startObject("type1")
                .startObject("properties")
                .startObject("name")
                    .field("type", "multi_field")
                    .field("path", "just_name")
                    .startObject("fields")
                        .startObject("name")
                            .field("type", "string")
                        .endObject()
                        .startObject("name_shingled")
                            .field("type", "string")
                            .field("index_analyzer", "biword")
                            .field("search_analyzer", "standard")
                        .endObject()
                    .endObject()
{'fraction_non_alphanumeric': 0.12148831224533561, 'fraction_numerical': 0.00922153120308814, 'mean_word_length': 2.8925292153589317, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 13, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '140330', 'n_tokens_mistral': 4986, 'n_tokens_neox': 4821, 'n_words': 1232}