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

直接获取_source在ElasticSearch中的实践

2024-07-134.2k 阅读

ElasticSearch 中的 _source 字段简介

在 ElasticSearch 中,_source 字段是一个非常重要的概念。当我们索引一个文档时,ElasticSearch 会将整个文档作为一个 JSON 对象存储在 _source 字段中。这个字段的存在使得我们在检索文档时能够方便地获取原始的文档内容,而无需额外的处理来重新构建文档结构。

例如,我们索引一个简单的博客文章文档:

{
    "title": "ElasticSearch 实践",
    "author": "John Doe",
    "content": "这是一篇关于 ElasticSearch 的文章。",
    "publish_date": "2023-10-01"
}

这个完整的 JSON 文档就会被存储在 _source 字段中。当我们执行搜索操作并希望获取这个文档的原始内容时,_source 字段就派上了用场。

直接获取 _source 的优势

  1. 简化数据获取流程:在很多场景下,我们只关心文档的原始内容,而不是经过 ElasticSearch 特定处理后的部分字段。直接获取 _source 可以避免复杂的字段提取逻辑。例如,在一个电商应用中,我们搜索商品时可能只需要获取商品的完整详情信息,直接获取 _source 就能快速得到商品的全部信息,包括描述、规格等所有原始数据。
  2. 减少数据处理开销:相比获取部分字段并重新组装成完整文档的方式,直接获取 _source 减少了 ElasticSearch 在服务端和客户端的数据处理开销。ElasticSearch 无需对文档进行额外的字段提取和格式化操作,客户端也无需进行复杂的字段拼接。
  3. 保持数据完整性_source 存储的是文档最初索引时的完整内容,获取 _source 可以确保我们得到的数据没有任何丢失或损坏,保证了数据的完整性。这在需要对文档进行精确处理,如数据迁移、文档归档等场景下非常重要。

在 ElasticSearch 中直接获取 _source 的方法

  1. 使用 REST API
    • 简单搜索获取 _source: 我们可以使用 ElasticSearch 的搜索 API 来直接获取 _source。例如,我们在名为 blog 的索引中搜索标题包含 “ElasticSearch” 的文章,并获取其 _source
      GET /blog/_search
      {
          "query": {
              "match": {
                  "title": "ElasticSearch"
              }
          }
      }
      
      上述请求返回的结果中,hits.hits 数组中的每个元素都包含 _source 字段,其中存储了匹配文档的原始内容。
    • 指定获取特定文档的 _source: 如果我们知道文档的 ID,想要获取特定文档的 _source,可以使用以下请求:
      GET /blog/_doc/{document_id}
      
      这里的 {document_id} 替换为实际的文档 ID。例如,如果文档 ID 为 “123”,请求为:
      GET /blog/_doc/123
      
      返回的结果中 _source 字段包含了该文档的完整原始内容。
  2. 使用 ElasticSearch 客户端(以 Python 的 Elasticsearch 库为例)
    • 安装 Elasticsearch 库: 首先需要安装 elasticsearch 库,可以使用 pip install elasticsearch 命令进行安装。
    • 简单搜索获取 _source
      from elasticsearch import Elasticsearch
      
      es = Elasticsearch([{'host': 'localhost', 'port': 9200}])
      query = {
          "query": {
              "match": {
                  "title": "ElasticSearch"
              }
          }
      }
      result = es.search(index='blog', body=query)
      for hit in result['hits']['hits']:
          print(hit['_source'])
      
      上述代码连接到本地的 ElasticSearch 实例,在 blog 索引中搜索标题包含 “ElasticSearch” 的文档,并打印出每个匹配文档的 _source 内容。
    • 指定获取特定文档的 _source
      from elasticsearch import Elasticsearch
      
      es = Elasticsearch([{'host': 'localhost', 'port': 9200}])
      document_id = '123'
      result = es.get(index='blog', id=document_id)
      print(result['_source'])
      
      这段代码获取 blog 索引中 ID 为 “123” 的文档的 _source 并打印出来。

直接获取 _source 的性能考虑

  1. 存储开销:由于 _source 存储了整个文档的原始内容,如果文档较大,会占用较多的磁盘空间。在设计索引时,需要权衡是否真的需要完整存储 _source。如果某些字段在检索时很少用到,可以考虑将其从 _source 中排除,通过 include_in_sourceexclude_from_source 参数来控制。例如:
    PUT /my_index
    {
        "mappings": {
            "properties": {
                "big_text_field": {
                    "type": "text",
                    "exclude_from_source": true
                },
                "other_field": {
                    "type": "keyword"
                }
            }
        }
    }
    
    上述映射将 big_text_field 字段排除在 _source 之外,这样可以减少 _source 的存储大小。
  2. 检索性能:一般来说,直接获取 _source 对于大多数场景是高效的。但如果索引数据量非常大,并且网络带宽有限,返回大量的 _source 数据可能会导致网络延迟增加。在这种情况下,可以考虑分页获取数据,或者只获取部分关键字段。例如,在使用 REST API 搜索时,可以通过 size 参数控制每页返回的文档数量:
    GET /blog/_search
    {
        "query": {
            "match_all": {}
        },
        "size": 10
    }
    
    上述请求每页只返回 10 个文档及其 _source,减少了单次返回的数据量。

与其他字段获取方式的对比

  1. 与获取部分字段对比
    • 获取部分字段:当我们只需要文档中的某些特定字段时,可以使用 _source 参数来指定。例如:
      GET /blog/_search
      {
          "query": {
              "match": {
                  "title": "ElasticSearch"
              }
          },
          "_source": ["title", "author"]
      }
      
      这个请求只返回匹配文档的 titleauthor 字段,而不是完整的 _source。这种方式适用于我们明确知道只需要少数几个字段,并且希望减少数据传输量的场景。
    • 对比分析:与直接获取 _source 相比,获取部分字段在数据量较小且明确知道需求时更节省带宽和处理资源。但如果需求不明确,或者后续可能需要更多字段,直接获取 _source 会更方便,避免了多次查询。
  2. 与脚本字段获取对比
    • 脚本字段获取:有时候我们需要通过计算得到一些新的字段,这时候可以使用脚本字段。例如:
      GET /blog/_search
      {
          "query": {
              "match": {
                  "title": "ElasticSearch"
              }
          },
          "script_fields": {
              "new_field": {
                  "script": {
                      "source": "doc['publish_date'].value + ' - ' + doc['author'].value"
                  }
              }
          }
      }
      
      上述请求通过脚本计算出一个新的 new_field 字段,值为 publish_dateauthor 字段值的拼接。
    • 对比分析:脚本字段获取适用于需要动态生成字段的场景,但计算过程会增加 ElasticSearch 的处理开销。而直接获取 _source 是简单地返回原始存储内容,性能更高,除非确实需要新生成的字段,否则直接获取 _source 更为合适。

在复杂场景下直接获取 _source 的应用

  1. 数据迁移:在将数据从一个 ElasticSearch 集群迁移到另一个集群时,直接获取 _source 可以确保完整的数据迁移。我们可以编写脚本,通过获取源集群文档的 _source,然后在目标集群中重新索引这些文档。例如,使用 Python 脚本:
    from elasticsearch import Elasticsearch
    
    source_es = Elasticsearch([{'host': 'source_host', 'port': 9200}])
    target_es = Elasticsearch([{'host': 'target_host', 'port': 9200}])
    
    index_name = 'my_index'
    search_result = source_es.search(index=index_name, body={"query": {"match_all": {}}})
    for hit in search_result['hits']['hits']:
        source_doc = hit['_source']
        target_es.index(index=index_name, body=source_doc)
    
    上述脚本从源 ElasticSearch 集群获取 my_index 索引的所有文档的 _source,并在目标集群中重新索引这些文档。
  2. 文档归档:在对 ElasticSearch 中的文档进行归档时,获取 _source 可以将文档以其原始格式保存下来。这对于审计、长期数据存储等场景非常有用。例如,我们可以将获取的 _source 内容写入文件进行归档:
    from elasticsearch import Elasticsearch
    import json
    
    es = Elasticsearch([{'host': 'localhost', 'port': 9200}])
    document_id = '123'
    result = es.get(index='blog', id=document_id)
    source_doc = result['_source']
    with open('archive.json', 'w') as f:
        json.dump(source_doc, f, indent=4)
    
    上述代码获取 blog 索引中 ID 为 “123” 的文档的 _source,并将其以 JSON 格式写入 archive.json 文件进行归档。

处理 _source 中的复杂数据结构

  1. 嵌套文档:如果 _source 中包含嵌套文档,获取和处理起来需要特别注意。例如,我们有一个包含评论的博客文章文档:
    {
        "title": "ElasticSearch 实践",
        "author": "John Doe",
        "content": "这是一篇关于 ElasticSearch 的文章。",
        "comments": [
            {
                "author": "Jane Smith",
                "text": "很棒的文章!"
            },
            {
                "author": "Bob Johnson",
                "text": "有些地方不太明白。"
            }
        ]
    }
    
    在获取这样的 _source 后,我们可以使用编程语言的相应数据结构来处理嵌套部分。以 Python 为例:
    from elasticsearch import Elasticsearch
    
    es = Elasticsearch([{'host': 'localhost', 'port': 9200}])
    document_id = '123'
    result = es.get(index='blog', id=document_id)
    source_doc = result['_source']
    for comment in source_doc['comments']:
        print(comment['author'] + ' 说: ' + comment['text'])
    
    上述代码获取包含嵌套评论的博客文章文档的 _source,并遍历评论部分进行打印。
  2. 数组字段:当 _source 中有数组字段时,同样需要根据需求进行处理。比如一个包含多个标签的博客文章:
    {
        "title": "ElasticSearch 实践",
        "author": "John Doe",
        "content": "这是一篇关于 ElasticSearch 的文章。",
        "tags": ["elasticsearch", "search", "big data"]
    }
    
    在 Python 中,我们可以这样处理:
    from elasticsearch import Elasticsearch
    
    es = Elasticsearch([{'host': 'localhost', 'port': 9200}])
    document_id = '123'
    result = es.get(index='blog', id=document_id)
    source_doc = result['_source']
    print('文章标签: ' + ', '.join(source_doc['tags']))
    
    这段代码获取包含标签数组的文档的 _source,并将标签数组以逗号分隔的字符串形式打印出来。

优化直接获取 _source 的策略

  1. 索引设计优化:在创建索引时,合理规划字段映射可以提高获取 _source 的性能。例如,避免过度嵌套和使用不必要的复杂数据类型。如果某些字段很少用于检索,并且不需要在 _source 中保留,可以将其排除。另外,对字段进行适当的压缩设置也可以减少 _source 的存储大小。例如,对于文本字段,可以使用 doc_values 来进行高效存储和检索,同时也能影响 _source 的存储和性能。
  2. 缓存策略:由于获取 _source 可能涉及到磁盘 I/O(如果数据不在内存中),可以考虑使用缓存机制。在应用层,可以使用诸如 Redis 这样的缓存工具,将经常访问的文档 _source 进行缓存。当请求获取 _source 时,首先检查缓存中是否存在,如果存在则直接返回,避免对 ElasticSearch 的重复请求。例如,使用 Python 和 Redis:
    from elasticsearch import Elasticsearch
    import redis
    
    es = Elasticsearch([{'host': 'localhost', 'port': 9200}])
    r = redis.Redis(host='localhost', port=6379, db=0)
    
    document_id = '123'
    cached_source = r.get(document_id)
    if cached_source:
        print(cached_source.decode('utf-8'))
    else:
        result = es.get(index='blog', id=document_id)
        source_doc = result['_source']
        r.set(document_id, json.dumps(source_doc))
        print(source_doc)
    
    上述代码在获取文档 _source 时,先检查 Redis 缓存中是否存在,如果不存在则从 ElasticSearch 获取并缓存到 Redis 中。

常见问题及解决方法

  1. _source 字段缺失:在某些情况下,可能会发现 _source 字段缺失。这可能是因为在索引文档时设置了 exclude_from_sourcetrue,或者在创建索引映射时没有正确配置 _source 相关参数。解决方法是检查索引映射,确保没有错误地排除了 _source 存储。例如,如果发现某个字段被错误地排除,可以修改索引映射:
    PUT /my_index/_mapping
    {
        "properties": {
            "field_to_include": {
                "type": "text",
                "include_in_source": true
            }
        }
    }
    
    上述请求将 field_to_include 字段重新包含到 _source 中。
  2. 获取 _source 性能慢:如果获取 _source 性能较慢,可能是由于索引数据量过大、磁盘 I/O 瓶颈或者网络问题。首先检查磁盘 I/O 情况,可以通过系统工具(如 iostat 等)查看磁盘读写性能。如果是网络问题,可以优化网络配置,如增加带宽、减少网络延迟。对于数据量过大的问题,可以考虑分页获取 _source,或者对索引进行分片和副本的合理配置,以提高查询性能。例如,通过调整分片数量来改善查询性能:
    PUT /my_index/_settings
    {
        "index": {
            "number_of_shards": 4
        }
    }
    
    上述请求将 my_index 索引的分片数量调整为 4 片,可能会改善大数据量下的查询性能,从而加快获取 _source 的速度。

通过以上对直接获取 _source 在 ElasticSearch 中的实践介绍,涵盖了其原理、优势、获取方法、性能考虑、与其他方式对比、复杂场景应用、数据结构处理、优化策略以及常见问题解决等方面,希望能帮助读者更好地在实际项目中应用这一功能,提高 ElasticSearch 的使用效率和效果。