Соединение SpringBoot Сводка по битве с Elasticsearch

Spring Boot

Помните онлайн-яму для майнинга запросов elasticsearch?

В первый раз, когда я использовал elasticsearch, я скопировал и вставил колесо из Интернета. Доброе утро, тест колеса завершен, и он онлайн. Однако через несколько дней я обнаружил, что время отклика интерфейса было высоким (тайм-аут по умолчанию — 500 мс), поэтому я продолжал оптимизировать код и сокращать время. Но в итоге код уже не поддается оптимизации, а время отклика по-прежнему не имеет явного тренда на снижение, а в пиковые часы даже серьезно зависает. Далее я медленно объясню использование и оптимизацию elasticsearch.

Spring Boot добавляет зависимость elasticsearch

Есть много вариантов на выбор: 1) Добавить зависимость от данных Spring. 2) Используйте клиентские зависимости, предоставленные elasticsearch. 3) Используйте зависимость jestClient. Разницы между первыми двумя нет, а третий — доступ к elasticsearch через http-запрос.

Используйте официальные зависимости elasticsearch

Просто проверьте elasticsearch при использовании IDE для инициализации Springboot, или вы можете напрямую добавить следующие зависимости:

<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
  <version>{elasticserch.version}</version>
		</dependency>

или кmavenВеб-сайт ищет зависимости, соответствующие версии elasticsearch:

<dependency>
			<groupId>org.elasticsearch</groupId>
			<artifactId>elasticsearch</artifactId>
			<version>{elasticserch.version}</version>
		</dependency>
		<dependency>
			<groupId>org.elasticsearch.client</groupId>
			<artifactId>transport</artifactId>
			<version>{elasticserch.version}</version>
		</dependency>

Следует отметить, что обязательно используйте зависимости, которые всегда были с вашей версией elasticsearch, иначе могут возникнуть ошибки.

Конфигурация эластичного поиска

@Configuration
public class ElasticsearchConfig {
    private static final Logger logger = LoggerFactory.getLogger(ElasticsearchConfig.class);

    @Value("${elasticsearch.port}")
    private String port;
    @Value("${elasticsearch.cluster.name}")
    private String clusterName;
    @Value("${elasticsearch.pool}")
    private String poolSize;
    @Value("${elasticsearch.ip}")
    private String esHost;

    @Bean(name = "transportClient")
    public TransportClient transportClient() {
        logger.info("Elasticsearch初始化开始。。。。。");
        TransportClient transportClient = null;
        try {
            // 配置信息
            Settings esSetting = Settings.builder()
                    //集群名字
                    .put("cluster.name", clusterName)
                    //增加嗅探机制,找到ES集群
                    .put("client.transport.sniff", true)
//                    .put("client.transport.ignore_cluster_name", true)
                    //增加线程池个数,暂时设为5
                    .put("thread_pool.search.size", Integer.parseInt(poolSize))
                    .build();
            //配置信息Settings自定义
            transportClient = new PreBuiltTransportClient(esSetting);
            TransportAddress transportAddress = new TransportAddress(InetAddress.getByName(esHost), Integer.valueOf(port));
            transportClient.addTransportAddresses(transportAddress);
            logger.info("连接elasticsearch");
        } catch (Exception e) {
            logger.error("elasticsearch TransportClient create error!!", e);
        }
        return transportClient;
    }
}

Более низкие версии elasticsearch будут отличаться при настройке пользовательского контента. Порт для подключения с помощью узла elasticsearch — 9300.

Простой в использовании:

@Component
public class ElasticsearchUtils {
    private static final Logger logger = LoggerFactory.getLogger(ElasticsearchUtils.class);

    @Resource(name = "transportClient")
    private TransportClient transportClient;

    private static TransportClient client;

    @PostConstruct
    public void init() {
        client = this.transportClient;
    }

    /**
     * @author xiaosen
     * @description 判断索引是否存在
     * @date 2019/1/23
     * @param
     * @return
     */
    public static boolean isIndexExist(String index) {
        IndicesExistsResponse inExistsResponse = client.admin().indices().exists(new IndicesExistsRequest(index)).actionGet();
        if (inExistsResponse.isExists()) {
            logger.info("索引:{}存在", index);
        } else {
            logger.info("索引:{}不存在", index);
        }
        return inExistsResponse.isExists();
    }
  
   public static List<Map<String, Object>> searchListData(String index, String type, long startTime, long endTime, Integer size, String fields, String sortField, boolean matchPhrase, String highlightField, String matchStr) {

        SearchRequestBuilder searchRequestBuilder = client.prepareSearch(index);
        if (StringUtils.isNotEmpty(type)) {
            searchRequestBuilder.setTypes(type.split(","));
        }
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();

        if (startTime > 0 && endTime > 0) {
            boolQuery.must(QueryBuilders.rangeQuery("processTime")
                    .format("epoch_millis")
                    .from(startTime)
                    .to(endTime)
                    .includeLower(true)
                    .includeUpper(true));
        }

        //搜索的的字段
        if (StringUtils.isNotEmpty(matchStr)) {
            for (String s : matchStr.split(",")) {
                String[] ss = s.split("=");
                if (ss.length > 1) {
                    if (matchPhrase == Boolean.TRUE) {
                        boolQuery.must(QueryBuilders.matchPhraseQuery(s.split("=")[0], s.split("=")[1]));
                    } else {
                        boolQuery.must(QueryBuilders.matchQuery(s.split("=")[0], s.split("=")[1]));
                    }
                }

            }
        }

        // 高亮(xxx=111,aaa=222)
        if (StringUtils.isNotEmpty(highlightField)) {
            HighlightBuilder highlightBuilder = new HighlightBuilder();
            // 设置高亮字段
            highlightBuilder.field(highlightField);
            searchRequestBuilder.highlighter(highlightBuilder);
        }


        searchRequestBuilder.setQuery(boolQuery);

        if (StringUtils.isNotEmpty(fields)) {
            searchRequestBuilder.setFetchSource(fields.split(","), null);
        }
        searchRequestBuilder.setFetchSource(true);

        if (StringUtils.isNotEmpty(sortField)) {
            searchRequestBuilder.addSort(sortField, SortOrder.DESC);
        }

        if (size != null && size > 0) {
            searchRequestBuilder.setSize(size);
        }
        SearchResponse searchResponse = searchRequestBuilder.execute().actionGet();

        long totalHits = searchResponse.getHits().totalHits;
        long length = searchResponse.getHits().getHits().length;

        if (searchResponse.status().getStatus() == 200) {
            // 解析对象
            return setSearchResponse(searchResponse, highlightField);
        }

        return null;

    }

Использование JestClient

Добавьте зависимости maven (версия elasticsearch здесь относительно низкая, а порт 9300 не открыт, можно использовать только http-запросы)

 <dependency>
            <groupId>org.elasticsearch</groupId>
            <artifactId>elasticsearch</artifactId>
            <version>1.5.2</version>
        </dependency>
        <dependency>
            <groupId>io.searchbox</groupId>
            <artifactId>jest</artifactId>
            <version>6.3.1</version>
        </dependency>

io.searchbox является зависимостью для работы elasticsearch и использует свой порт 9200.

Конфигурационный файл относительно прост:

@Configuration
@RefreshScope
public class ElasticsearchConfigure {
    private static final Logger logger = LoggerFactory.getLogger(ElasticsearchConfigure.class);

    @Value("${elasticsearch.ip}")
    private String hostAndPort;

    @Bean(name = "elasticsearchClient")
    public JestClient getJestClient() throws Exception{
        JestClientFactory factory = new JestClientFactory();
        SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, (X509Certificate[] arg0, String arg1) -> true).build();
      // http配置
        factory.setHttpClientConfig(new HttpClientConfig.Builder("http://"+hostAndPort).connTimeout(2000)
                .readTimeout(2000).plainSocketFactory(PlainConnectionSocketFactory.getSocketFactory())
                .sslSocketFactory(new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE))
                .multiThreaded(true).maxTotalConnection(100).defaultMaxTotalConnectionPerRoute(4).build());
        return factory.getObject();
    }
}

Создайте JestClientFactory и настройте httpClient.

Простой пример:

@Resource(name = "elasticsearchClient")
    private JestClient jestClient;

public static void main(String[] args){
  FilterBuilder filterBuilder = FilterBuilders.boolFilter()
                    .must(FilterBuilders.geoDistanceRangeFilter("location")
                            .point(lat, lon).from(Constants.MIN_RADIUS).to(Constants.MAX_RADIUS))
                    .should(FilterBuilders.termFilter("status", 200), FilterBuilders.termFilter("status", 201));
  FilteredQueryBuilder filteredQueryBuilder = new FilteredQueryBuilder(null, filterBuilder);
            // 按在线时间排序,先按时间再按距离排序
            FieldSortBuilder sortBuilderField = SortBuilders.fieldSort("time").order(SortOrder.DESC);
            // 按距离排序,为返回客户端距离,返回的单位:米
            GeoDistanceSortBuilder sortBuilderDis = SortBuilders.geoDistanceSort("location").point(lat, lon).unit(DistanceUnit.KILOMETERS).order(SortOrder.ASC).geoDistance(GeoDistance.SLOPPY_ARC);
   SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
  searchSourceBuilder.query(filteredQueryBuilder).sort(sortBuilderField).sort(sortBuilderDis)
                    .from((queryNearbyDto.getCurrentPage()-1)*queryNearbyDto.getPageSize())
                    .size(queryNearbyDto.getPageSize()).fetchSource(Constants.QUERY_FIELD_TTID, null);
            String query = searchSourceBuilder.toString();
  result = search(jestClient, index, Constants.ES_NEARBY_TYPE, query);
  
}



private List<Map<String, Object>> search(JestClient jestClient, String indexName, String typeName, String query) throws Exception {
        Search search = new Search.Builder(query).setSearchType(SearchType.QUERY_THEN_FETCH)
                .addIndex(indexName)
                .addType(typeName)
                .build();
        SearchResult jr = jestClient.execute(search);
        if (!jr.isSucceeded()||jr.getResponseCode()!=200){
            return null;
        }
        Long total = jr.getTotal();
        List<SearchResult.Hit<Map, Void>> maps = jr.getHits(Map.class, false);
        List<Map<String, Object>> sourceList = maps.stream().map(source -> {
            source.source.put("sort", Double.valueOf(source.sort.get(1)));
            return (Map<String, Object>)source.source;
        }).collect(Collectors.toList());
        return sourceList;
    }

Переменный запрос — это оператор запроса elasticsearch.Если вы знаете синтаксис elasticsearch, вместо этого вы можете напрямую написать json.

Сортировать по расстоянию

В jestClient есть пример сортировки по расстоянию и времени, который заключается в сортировке по времени, а затем по расстоянию, чтобы вернуть расстояние. es могут быть отсортированы по нескольким полям, первое — это сортировка по приоритету, окончательный результат сортировки будет возвращен в возвращаемом массиве сортировки, позиция в массиве — это позиция соответствия сортировки, я извлеку возвращаемое расстояние здесь и поставлю это в карту.

Метод сортировки по расстоянию API elasticsearch версии 5.2 выглядит следующим образом:

GeoDistanceSortBuilder sortBuilderDis = SortBuilders.geoDistanceSort("location", lat, lon).point(lat, lon).unit(DistanceUnit.METERS).order(SortOrder.ASC).geoDistance(GeoDistance.ARC);

Если вы не хотите, чтобы elasticsearch вычислял расстояние, вы также можете использовать предоставленный им метод, чтобы вычислить его самостоятельно, при условии, что вы знаете широту и долготу двух, вызовите метод расчета GeoDistance, можно выбрать конкретную точность в соответствии с бизнес-требованиями, но я провел тесты и рассчитал сам Расстояние почти такое же, как время расчета elasticsearch.Если это дополнительное расстояние расчета, вы больше не можете снова проверять elasticsearch, чтобы уменьшить потребление ввода-вывода.

нумерация страниц

Для студентов, не знакомых с elasticsearch, пейджинг — тоже яма.

Неглубокая нумерация страниц

Неглубокая подкачка elasticsearch from & size, from — индексная позиция запроса, а size — количество страниц на странице.Преимущества аналогичны лимиту и старту mysql.

Теперь мы можем предположить поиск в индексе с 5 первичными шардами. Когда мы запрашиваем первую страницу результатов (результаты от 1 до 10), каждый сегмент выдает 10 лучших результатов и возвращает их вузел координатора, координирующий узел сортирует 50 результатов, чтобы получить первые 10 из всех результатов. Теперь предположим, что мы запрашиваем страницу 1000 — результаты от 10001 до 10010. Все работает одинаково, за исключением того, что каждый сегмент должен выдать первые 10010 результатов. Затем координирующий узел сортирует все 50050 результатов и, наконец, отбрасывает 50040 из этих результатов. Вы можете видеть, что в распределенной системе стоимость сортировки результатов увеличивается экспоненциально с глубиной разбиения по страницам. Вот почему поисковые системы никогда не возвращают более 1000 результатов по любому запросу. Когда вы перелистываете страницы, чем глубже вы перелистываете, тем больше данных возвращает каждый шард и тем дольше обрабатывается координационный узел, что очень раздражает. Поэтому, когда вы используете ES для пейджинга, вы обнаружите, что чем больше вы поворачиваетесь назад, тем медленнее он будет. Мы также сталкивались с этой проблемой раньше.При использовании ES для подкачки первые несколько страниц занимают всего несколько десятков миллисекунд.Когда мы переходим к 10 или десяткам страниц, в основном требуется 5-10 секунд, чтобы найти страницу данных.

Максимальный размер запроса с использованием from&size — 10 000 единиц данных, это значение можно установить в файле конфигурации в elasticsearch.

прокрутить глубокую прокрутку

Чтобы решить вышеуказанные проблемы, elasticsearch предлагает метод прокрутки. Прокрутка аналогична курсору в sql.Используя прокрутку, вы можете получить содержимое только одной страницы за раз, а затем вернуть scroll_id. По возвращаемому scroll_id содержимое следующей страницы может быть получено непрерывно, поэтому прокрутка не подходит для ситуаций с переходами по страницам.

POST /twitter/_search?scroll=1m
{
    "size": 100,
    "query": {
        "match" : {
            "title" : "elasticsearch"
        }
    }
}
  1. scroll=1m означает, что scroll_id будет доступен в течение 1 минуты.
  2. Чтобы использовать прокрутку, from должен быть установлен на 0.
  3. size определяет число, возвращаемое каждым последующим вызовом _search
POST /_search/scroll 
{
    "scroll" : "1m", 
    "scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==" 
}

Затем мы можем прочитать содержимое следующей страницы через _scroll_id, возвращаемый данными.Каждый запрос будет считывать следующие 10 фрагментов данных, пока данные не будут прочитаны или не истечет время хранения scroll_id. Запрашиваемый интерфейс больше не использует имя индекса, а использует _search/scroll, где можно использовать как методы GET, так и методы POST.

search_after

Прокрутка рекомендуется для глубоких запросов, но контексты дороги, не рекомендуются для пользовательских запросов в реальном времени, но больше подходят для фоновых пакетных задач, таких как пакетная обработка. search_after предоставляет курсор в реальном времени, чтобы избежать глубоких проблем с разбиением на страницы, идея состоит в том, чтобы использовать результаты предыдущей страницы, чтобы помочь получить следующую страницу. search_after не может свободно переходить на случайную страницу, он может переходить на следующую страницу только в соответствии со значениями сортировки. При использовании параметра search_after параметр from должен быть установлен на 0 или -1 (конечно, вы также не можете установить этот параметр from)

search_after должен использовать поле с уникальным значением в качестве поля сортировки, иначе метод search_after не может быть использован. Рекомендуется использовать _uid в качестве поля сортировки уникальных значений.

GET twitter/_search
{
    "size": 10,
    "query": {
        "match" : {
            "title" : "elasticsearch"
        }
    },
    "sort": [
        {"date": "asc"},
        {"tie_breaker_id": "asc"}      
    ]
}

В следующем запросе отсортированный массив последних возвращенных данных помещается в search_after.

GET twitter/_search
{
    "size": 10,
    "query": {
        "match" : {
            "title" : "elasticsearch"
        }
    },
    "search_after": [1463538857, "654323"],
    "sort": [
        {"date": "asc"},
        {"tie_breaker_id": "asc"}
    ]
}

Суммировать

  • Глубокое разбиение по страницам, будь то реляционная база данных, Elasticsearch или другие поисковые системы, приведет к огромным потерям производительности, особенно в распределенных ситуациях.
  • Некоторые проблемы можно решить бизнесом, а не технологиями, например, у многих предприятий есть ограничения на номера страниц, поиск в Google, а затем переход на определенный номер страницы не сработает.
  • прокрутка не подходит для поиска в реальном времени, но больше подходит для фоновых пакетных задач, таких как групповая отправка.
  • search_after не может свободно переходить на случайную страницу, он может переходить на следующую страницу только в соответствии со значениями сортировки.

Сортировка и релевантность

По умолчанию возвращаемые результаты сортируются по релевантности — наиболее релевантные документы идут первыми. Каждый документ имеет оценку релевантности с положительным полем с плавающей запятой._scoreПредставлять ._scoreЧем выше балл, тем выше корреляция.

Оператор запроса будет генерировать_scoreполе. Способ расчета оценки зависит от типа запроса. Различные операторы запроса используются для разных целей:fuzzyЗапрос вычисляет сходство написания с ключевым словом,termsЗапрос вычисляет процент найденного контента, который соответствует компонентам ключевого слова, но обычно мы говоримrelevance— это алгоритм, который мы используем для расчета того, насколько похожи значения полнотекстового поля на полнотекстовый термин.

Конкретный алгоритм оценки можно запросить на официальном сайте.

Установить в коде:

// 设置是否按查询匹配度排序
searchRequestBuilder.setExplain(true);

Уведомление:

Сортировка связанных элементов требует много ресурсов. Если требования к точности текста не особенно высоки, сортировка по релевантности не рекомендуется в производственной среде.

Ссылаться на:

woohoo.elastic.co/expensive/en/bad…

blog.CSDN.net/Один за другим хорошая позиция/Ах…

Woohoo.Soyunku.com/2017/11/06/…

woo woo woo.cn blog on.com/Yang Zhen you…

woohoo.elastic.co/дорогой/талантливый/плохой…


Добро пожаловать в публичный аккаунт:

公众号微信