MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

ElasticSearch模糊性API在文本搜索中的调优

2022-01-127.5k 阅读

ElasticSearch模糊性API基础

在文本搜索领域,ElasticSearch凭借其强大的搜索功能和分布式架构成为了众多开发者的首选。其中,模糊性API为处理用户输入的不精确查询提供了有力支持。模糊性搜索允许我们在搜索时考虑到单词的拼写错误、相似词等情况,从而返回更符合用户预期的结果。

ElasticSearch中的模糊查询主要通过fuzzy查询来实现。例如,我们有一个简单的索引结构,包含一个名为content的字段,存储文本内容:

PUT my_index
{
    "mappings": {
        "properties": {
            "content": {
                "type": "text"
            }
        }
    }
}

然后我们插入一些文档:

POST my_index/_doc
{
    "content": "ElasticSearch is a powerful search engine"
}
POST my_index/_doc
{
    "content": "Learn to use ElasticSearch for better search results"
}

现在,如果我们想进行模糊查询,假设用户输入可能有误,我们可以这样写查询:

GET my_index/_search
{
    "query": {
        "fuzzy": {
            "content": {
                "value": "ElastcSearch",
                "fuzziness": "AUTO"
            }
        }
    }
}

在这个查询中,fuzziness参数指定了允许的模糊程度。这里设置为AUTO,ElasticSearch会根据单词的长度自动确定模糊程度。一般来说,较短的单词允许较小的编辑距离,较长的单词允许较大的编辑距离。

模糊性API的核心参数剖析

fuzziness参数

fuzziness参数是控制模糊查询的关键。它有几种取值方式:

  1. 具体数字:例如fuzziness": 2,表示允许单词有2个字符的编辑距离。编辑距离通常指的是通过插入、删除或替换字符,将一个单词转换为另一个单词所需的最少操作数。
  2. AUTO:正如前面例子所示,AUTO会根据单词长度自动确定模糊程度。具体规则是:长度小于3的单词,编辑距离为0;长度在3到5之间的单词,编辑距离为1;长度大于5的单词,编辑距离为2。
  3. AUTO:min,max:这种方式允许我们自定义自动计算的最小和最大编辑距离。例如"fuzziness": "AUTO:1,2",表示对于长度小于3的单词,编辑距离至少为1;对于长度大于5的单词,编辑距离最大为2。

prefix_length参数

prefix_length参数用于指定单词前缀必须精确匹配的长度。例如:

GET my_index/_search
{
    "query": {
        "fuzzy": {
            "content": {
                "value": "ElastcSearch",
                "fuzziness": "AUTO",
                "prefix_length": 3
            }
        }
    }
}

在这个查询中,单词的前3个字符Ela必须精确匹配,只有后面的部分才会进行模糊匹配。这样可以减少匹配的范围,提高查询效率,特别是在索引数据量较大时。

max_expansions参数

max_expansions参数限制了模糊查询中一个单词可能扩展出的最大项数。当fuzziness允许较高的编辑距离时,一个单词可能会扩展出大量的变体。例如,一个单词允许编辑距离为2,可能会产生几十甚至上百种变体。通过设置max_expansions,我们可以限制扩展的数量,避免查询过于耗费资源。例如:

GET my_index/_search
{
    "query": {
        "fuzzy": {
            "content": {
                "value": "ElastcSearch",
                "fuzziness": "AUTO",
                "max_expansions": 10
            }
        }
    }
}

这意味着最多只考虑10种模糊变体,超过这个数量的变体将被忽略。

文本搜索中的性能问题及调优方向

在使用ElasticSearch模糊性API进行文本搜索时,性能问题是一个常见的挑战。随着索引数据量的增加和模糊程度的提高,查询的响应时间可能会显著变长。主要的性能瓶颈包括以下几个方面:

计算编辑距离的开销

计算两个单词之间的编辑距离是一个相对复杂的操作,特别是当fuzziness值较大时。对于每个查询词,ElasticSearch需要计算其与索引中大量单词的编辑距离,这会消耗大量的CPU资源。

索引数据量的影响

索引中的文档数量越多,模糊查询需要遍历的数据量就越大。即使使用了合理的索引结构和分片策略,当数据量达到一定规模时,模糊查询的性能仍然会受到影响。

网络传输开销

如果ElasticSearch集群分布在多个节点上,模糊查询可能需要在多个节点之间传输数据,这会带来额外的网络延迟。

针对这些性能问题,我们可以从以下几个方向进行调优:

基于索引结构的调优

合理设置字段类型

在定义索引映射时,选择合适的字段类型对于模糊查询性能至关重要。对于需要进行模糊搜索的文本字段,text类型是常用的选择。但是,text类型默认会进行分词,这可能会导致一些问题。例如,如果我们希望对整个单词进行模糊匹配,而分词后单词被拆分成多个部分,可能会影响查询结果。在这种情况下,可以考虑使用keyword类型,并结合fielddata来支持模糊查询。

PUT my_index
{
    "mappings": {
        "properties": {
            "content": {
                "type": "keyword",
                "fielddata": true
            }
        }
    }
}

使用keyword类型时,整个字段值会作为一个完整的词条进行索引,不会被分词。fielddata则允许在这个字段上进行基于内存的搜索,提高模糊查询性能。不过,需要注意的是,fielddata会占用较多的内存,所以在使用时要根据实际情况权衡。

优化分词器

如果仍然使用text类型,优化分词器可以显著提高模糊查询性能。例如,对于英文文本,可以选择english分词器,它会对英文单词进行更合理的切分,并且会去除一些停用词(如theand等)。

PUT my_index
{
    "mappings": {
        "properties": {
            "content": {
                "type": "text",
                "analyzer": "english"
            }
        }
    }
}

对于中文文本,可以使用ik分词器等专门针对中文的分词器。ik分词器有两种模式:ik_smartik_max_wordik_smart模式会进行较为精准的分词,而ik_max_word模式会尽可能细粒度地分词。根据业务需求选择合适的分词器模式,可以减少不必要的词条数量,提高查询效率。

索引分片与副本的调整

ElasticSearch通过分片和副本机制来实现数据的分布式存储和高可用性。在进行模糊查询时,分片数量和副本数量会影响查询性能。如果分片数量过多,每个分片的数据量较小,可能会增加查询时的网络开销和协调成本。相反,如果分片数量过少,单个分片的数据量过大,会影响查询的并行处理能力。 一般来说,对于较小规模的数据集,可以适当减少分片数量;对于大规模数据集,需要根据实际情况进行测试和调整。例如,在一个拥有100GB数据的集群中,初始设置10个分片可能性能较好,但随着数据量增长到500GB,可能需要增加到20个分片。 副本数量主要影响系统的容错性和读性能。增加副本数量可以提高读性能,但会占用更多的存储空间和网络带宽。在进行模糊查询时,如果读操作频繁,可以适当增加副本数量,但也要注意资源的消耗。

模糊查询参数的优化

精细调整fuzziness

在设置fuzziness参数时,要根据业务需求和数据特点进行精细调整。如果fuzziness值过大,会增加匹配的变体数量,导致查询性能下降;如果值过小,可能会遗漏一些可能的匹配结果。例如,在一个商品搜索系统中,用户输入商品名称时可能会有一些拼写错误,但错误通常不会太离谱。这时可以将fuzziness设置为1或根据单词长度采用AUTO模式,并适当调整AUTO的最小和最大编辑距离。

GET product_index/_search
{
    "query": {
        "fuzzy": {
            "product_name": {
                "value": "iphne 14",
                "fuzziness": "AUTO:1,2"
            }
        }
    }
}

这样既可以考虑到常见的拼写错误,又不会因为过度模糊导致查询结果过多且不准确。

合理利用prefix_length

通过设置合适的prefix_length,可以显著减少模糊匹配的范围,提高查询效率。在很多情况下,用户输入的前缀往往是比较准确的。例如,在搜索人名时,用户输入“Joh”,很可能是想搜索以“Joh”开头的名字。这时可以设置prefix_length为3,只对“Joh”之后的部分进行模糊匹配。

GET person_index/_search
{
    "query": {
        "fuzzy": {
            "name": {
                "value": "Johhny",
                "fuzziness": "AUTO",
                "prefix_length": 3
            }
        }
    }
}

这样可以避免对大量不相关的名字进行模糊匹配,从而加快查询速度。

控制max_expansions

max_expansions参数对于控制查询资源消耗非常重要。如果不设置这个参数,当fuzziness较大时,一个单词可能会扩展出大量变体,导致查询性能急剧下降。在实际应用中,要根据索引数据量和查询性能要求来合理设置max_expansions。例如,在一个包含10万条文档的索引中,对于模糊查询可以将max_expansions设置为20到50之间,既保证有足够的变体被考虑,又不会让查询过于耗费资源。

GET news_index/_search
{
    "query": {
        "fuzzy": {
            "title": {
                "value": "techology news",
                "fuzziness": "AUTO",
                "max_expansions": 30
            }
        }
    }
}

基于缓存机制的调优

节点级缓存

ElasticSearch提供了节点级缓存,用于缓存查询结果。对于一些频繁执行的模糊查询,可以利用节点级缓存来提高性能。节点级缓存主要包括过滤器缓存和字段数据缓存。 过滤器缓存用于缓存过滤器查询的结果。例如,如果我们有一个经常使用的模糊查询,并且查询条件相对固定,可以通过将其包装在过滤器中,利用过滤器缓存来提高查询性能。

GET my_index/_search
{
    "query": {
        "bool": {
            "filter": {
                "fuzzy": {
                    "content": {
                        "value": "specific_term",
                        "fuzziness": "AUTO"
                    }
                }
            }
        }
    }
}

字段数据缓存则用于缓存字段数据,特别是对于keyword类型且启用了fielddata的字段。当对这些字段进行模糊查询时,字段数据缓存可以减少从磁盘读取数据的次数,提高查询效率。不过,需要注意的是,节点级缓存会占用节点的内存资源,要根据节点的内存情况合理配置缓存大小。

分布式缓存

除了节点级缓存,还可以考虑使用分布式缓存来进一步提高模糊查询性能。例如,使用Redis等分布式缓存系统。在ElasticSearch执行模糊查询之前,先检查分布式缓存中是否已经存在查询结果。如果存在,则直接返回缓存中的结果;如果不存在,则执行查询,并将结果存入分布式缓存中。 以下是一个简单的Java代码示例,展示如何结合ElasticSearch和Redis实现缓存功能:

import redis.clients.jedis.Jedis;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.builder.SearchSourceBuilder;

public class ElasticSearchWithRedisCache {
    private static final String REDIS_KEY_PREFIX = "es_fuzzy_query_";
    private final RestHighLevelClient esClient;
    private final Jedis redisClient;

    public ElasticSearchWithRedisCache(RestHighLevelClient esClient, Jedis redisClient) {
        this.esClient = esClient;
        this.redisClient = redisClient;
    }

    public SearchResponse executeFuzzyQuery(String index, String field, String value, int fuzziness) {
        String redisKey = REDIS_KEY_PREFIX + index + "_" + field + "_" + value + "_" + fuzziness;
        String cachedResult = redisClient.get(redisKey);
        if (cachedResult != null) {
            // 这里需要将缓存的字符串反序列化为SearchResponse对象,实际应用中需要根据具体的序列化方式实现
            // 简单示例,假设已经有反序列化方法deserializeSearchResponse
            return deserializeSearchResponse(cachedResult);
        }

        SearchRequest searchRequest = new SearchRequest(index);
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(QueryBuilders.fuzzyQuery(field, value).fuzziness(fuzziness));
        searchRequest.source(searchSourceBuilder);

        try {
            SearchResponse searchResponse = esClient.search(searchRequest, RequestOptions.DEFAULT);
            // 将查询结果序列化并存入Redis,实际应用中需要根据具体的序列化方式实现
            // 简单示例,假设已经有序列化方法serializeSearchResponse
            redisClient.set(redisKey, serializeSearchResponse(searchResponse));
            return searchResponse;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

通过这种方式,可以大大减少ElasticSearch的查询压力,提高模糊查询的响应速度。

分布式查询的优化

分片查询并行化

在ElasticSearch集群中,模糊查询会被分发到各个分片上执行。为了提高查询性能,可以优化分片查询的并行化策略。ElasticSearch默认会并行处理多个分片的查询,但在一些情况下,我们可以进一步调整并行度。例如,通过设置search.max_concurrent_shard_requests参数来控制每个节点同时处理的分片请求数量。

# elasticsearch.yml配置文件
search.max_concurrent_shard_requests: 5

适当增加这个参数的值,可以提高查询的并行处理能力,但也要注意不要设置过高,以免耗尽节点资源。

跨节点数据合并优化

当模糊查询涉及多个节点时,查询结果需要在各个节点之间进行合并。在这个过程中,网络传输和数据合并的效率会影响整体查询性能。为了优化跨节点数据合并,可以采用以下方法:

  1. 减少不必要的数据传输:在查询时,只返回需要的字段,避免传输大量无关数据。例如,使用_source字段来指定返回的字段。
GET my_index/_search
{
    "_source": ["title", "content"],
    "query": {
        "fuzzy": {
            "content": {
                "value": "specific_term",
                "fuzziness": "AUTO"
            }
        }
    }
}
  1. 优化数据合并算法:ElasticSearch内部有自己的数据合并算法,但在一些复杂的模糊查询场景下,可能需要对其进行优化。例如,通过自定义脚本或插件来实现更高效的数据合并逻辑,以减少合并过程中的资源消耗和时间开销。

实时性与性能的平衡

在一些应用场景中,需要在实时性和性能之间进行平衡。例如,在实时搜索系统中,用户希望能够立即看到最新的搜索结果,但过高的实时性要求可能会影响模糊查询的性能。

刷新策略调整

ElasticSearch通过刷新操作将内存中的数据写入磁盘,从而使新数据可见。默认情况下,ElasticSearch会每隔1秒自动刷新一次。对于模糊查询性能敏感的应用,可以适当调整刷新策略。例如,将自动刷新间隔设置为5秒或更长时间,这样可以减少刷新操作的频率,提高查询性能。

PUT my_index/_settings
{
    "refresh_interval": "5s"
}

不过,这样做会导致新数据在索引中可见的延迟增加,需要根据业务需求进行权衡。

近实时搜索方案

如果既希望保持较高的实时性,又要保证模糊查询性能,可以采用近实时搜索方案。例如,使用search_type=dfs_query_then_fetch来进行搜索。这种搜索类型会先在所有分片上执行查询(dfs_query阶段),收集词频等信息,然后再在每个分片上执行实际的查询并获取结果(fetch阶段)。虽然这种方式会增加一些查询开销,但可以在一定程度上提高实时性和查询准确性。

GET my_index/_search?search_type=dfs_query_then_fetch
{
    "query": {
        "fuzzy": {
            "content": {
                "value": "specific_term",
                "fuzziness": "AUTO"
            }
        }
    }
}

另外,还可以结合使用index.refresh API来手动刷新索引,在需要及时获取最新数据时调用,而不是依赖自动刷新机制,从而在实时性和性能之间找到更好的平衡点。

通过上述对ElasticSearch模糊性API在文本搜索中的深入剖析和调优策略,我们可以在不同的业务场景下,根据实际需求和资源情况,优化模糊查询的性能,提供更高效、准确的搜索体验。无论是从索引结构的优化、查询参数的调整,还是缓存机制、分布式查询以及实时性与性能平衡等方面,每一个环节都对整体搜索性能有着重要影响,需要开发者在实践中不断探索和优化。