ElasticSearch

views 2163 words

ElasticSearch

一个开源的全文搜索引擎,可以快速地储存、搜索和分析海量数据. 建立在一个全文搜索引擎库 Apache Lucene™(目前存在的最先进,高性能和全功能搜索引擎功能的库) 基础之上. Elasticsearch 也是使用 Java 编写的,它的内部使用 Lucene 做索引与搜索,但是它的目标是使全文检索变得简单,相当于 Lucene 的一层封装,它提供了一套简单一致的 RESTful API 来帮助实现存储和检索, 可以使用 curl 等命令来进行操作.

  • 一个分布式的实时文档存储,每个字段可以被索引与搜索
  • 一个分布式实时分析搜索引擎
  • 支持上百个服务节点的扩展,并支持 PB 级别的结构化或者非结构化数据
  • 维基百科、Stack Overflow、GitHub …都用它来做搜索

Mac安装

brew install elasticsearch

上面安装的版本不是最新的, 使用下面 to install the most recently released default distribution of Elasticsearch:

brew tap elastic/tap

brew install elastic/tap/elasticsearch-full

在terminal输入

elasticsearch

Elasticsearch 默认会在 9200 端口上运行,打开浏览器访问 http://localhost:9200/ 就可以看到下面内容:

{
  "name" : "uZQ0yXL",
  "cluster_name" : "elasticsearch_Charon",
  "cluster_uuid" : "PjxtvieMSAOyhb91H29-Vg",
  "version" : {
    "number" : "6.8.8",
    "build_flavor" : "oss",
    "build_type" : "tar",
    "build_hash" : "2f4c224",
    "build_date" : "2020-03-18T23:22:18.622755Z",
    "build_snapshot" : false,
    "lucene_version" : "7.7.2",
    "minimum_wire_compatibility_version" : "5.6.0",
    "minimum_index_compatibility_version" : "5.0.0"
  },
  "tagline" : "You Know, for Search"
}

Elasticsearch 相关概念

Node 和 Cluster

Elasticsearch 本质上是一个分布式数据库,允许多台服务器协同工作,每台服务器可以运行多个 Elasticsearch 实例.

单个 Elasticsearch 实例称为一个节点(Node). 一组节点(Node)构成一个集群(Cluster).

Index

Elasticsearch 会索引所有字段,经过处理后写入一个反向索引(Inverted Index). 查找数据的时候,直接查找该索引.

所以,Elasticsearch 数据管理的顶层单位就叫做 Index(索引), 其实就相当于 MySQL、MongoDB 等里面的数据库的概念. 另外值得注意的是,每个 Index (即数据库)的名字必须是小写.

Document

Index 里面单条的记录称为 Document(文档). 许多条 Document 构成了一个 Index.

Document 使用 JSON 格式表示.

同一个 Index 里面的 Document,不要求有相同的结构(scheme), 但是最好保持相同,这样有利于提高搜索效率.

Type

Document 可以分组,比如 weather 这个 Index 里面,可以按城市分组(北京和上海),也可以按气候分组(晴天和雨天). 这种分组就叫做 Type,它是虚拟的逻辑分组,用来过滤 Document,类似 MySQL 中的数据表,MongoDB 中的 Collection.

不同的 Type 应该有相似的结构(Schema), 举例来说,id 字段不能在这个组是字符串,在另一个组是数值. 这是与关系型数据库的表的一个区别. 性质完全不同的数据(比如 products 和 logs)应该存成两个 Index,而不是一个 Index 里面的两个 Type(虽然可以做到).

Fields

即字段,每个 Document 都类似一个 JSON 结构,它包含了许多字段,每个字段都有其对应的值,多个字段组成了一个 Document,其实就可以类比 MySQL 数据表中的字段.

小结

在 Elasticsearch 中,文档归属于一种类型(Type),而这些类型存在于索引(Index)中,可以画一些简单的对比图来类比传统关系型数据库:

Relational DB -> Databases -> Tables -> Rows -> Columns
Elasticsearch -> Indices   -> Types  -> Documents -> Fields

Python 对接 Elasticsearch

安装

pip3 install elasticsearch

创建Index

from elasticsearch import Elasticsearch

es = Elasticsearch()
res = es.indices.create(index='news',ignore=400)
print(res)

如果创建成功, 会返回:

{'acknowledged': True, 'shards_acknowledged': True, 'index': 'news'}

返回结果是 JSON 格式,其中的 acknowledged 字段表示创建操作执行成功.

但这时如果再把代码执行一次的话,就会返回如下结果

{'error': {'root_cause': [{'type': 'resource_already_exists_exception', 'reason': 'index [news/mfvgAx7fRJ6T2gFeNd-MtA] already exists', 'index_uuid': 'mfvgAx7fRJ6T2gFeNd-MtA', 'index': 'news'}], 'type': 'resource_already_exists_exception', 'reason': 'index [news/mfvgAx7fRJ6T2gFeNd-MtA] already exists', 'index_uuid': 'mfvgAx7fRJ6T2gFeNd-MtA', 'index': 'news'}, 'status': 400}

它提示创建失败,status 状态码是 400,错误原因是 Index 已经存在了

注意: 这里的代码里面使用了 ignore 参数为 400,这说明如果返回结果是 400 的话,就忽略这个错误不会报错,程序不会执行抛出异常

假如我们不加 ignore 这个参数的话, 再次运行就会报错:

elasticsearch.exceptions.RequestError: RequestError(400, 'resource_already_exists_exception', 'index [news/mfvgAx7fRJ6T2gFeNd-MtA] already exists')

这样程序的执行就会出现问题,所以要善用 ignore 参数,把一些意外情况排除,这样可以保证程序的正常执行而不会中断

删除Index

res = es.indices.delete(index='news',ignore=[400,404])
print(res)  # {'acknowledged': True}

这里也是使用了 ignore 参数,来忽略 Index 不存在而删除失败导致程序中断的问题. 同样, 去除 ignore , 且再次删除就会报错.

插入数据

Elasticsearch 就像 MongoDB 一样,在插入数据的时候可以直接插入结构化字典数据,插入数据可以调用 create() 方法,例如插入一条新闻数据:

from elasticsearch import Elasticsearch

es = Elasticsearch()

res = es.indices.create(index='news',ignore=400)
print(res)

data = {'title': 'Coronavirus: Wuhan in first virus cluster since end of lockdown', 'url': 'https://www.bbc.com/news/world-asia-china-52613138'}
result = es.create(index='news', doc_type='politics', id=1, body=data)
print(result)

通过调用 create() 方法插入数据,在调用 create() 方法时,传入了四个参数,

  • index 参数代表了索引名称
  • doc_type 代表了文档类型
  • body 则代表了文档具体内容
  • id 则是数据的唯一标识 ID

运行结果如下:

{'_index': 'news', '_type': 'politics', '_id': '1', '_version': 1, 'result': 'created', '_shards': {'total': 2, 'successful': 1, 'failed': 0}, '_seq_no': 0, '_primary_term': 1}

结果中 result 字段为 created,代表该数据插入成功.

另外也可以使用 index() 方法来插入数据,但与 create() 不同的是,create() 方法需要指定 id 字段来唯一标识该条数据,而 index() 方法则不需要,如果不指定 id,会自动生成一个 id,调用 index() 方法的写法如下:

es.index(index='news', doc_type='politics', body=data)

create() 方法内部其实也是调用了 index() 方法,是对 index() 方法的封装

更新数据

同样需要指定数据的 id 和内容,调用 update() 方法即可,代码如下:

es = Elasticsearch()
data = {
    'title': 'Coronavirus: Wuhan in first virus cluster since end of lockdown',
    'url': 'https://www.bbc.com/news/world-asia-china-52613138',
    'date': '2020-05-11'
}
result = es.update(index='news', doc_type='politics', body=data, id=1)
print(result)

这里为数据增加了一个(date)日期字段,然后调用了 update() 方法,结果如下:

1
{'_index': 'news', '_type': 'politics', '_id': '1', '_version': 2, 'result': 'updated', '_shards': {'total': 2, 'successful': 1, 'failed': 0}, '_seq_no': 1, '_primary_term': 1}

可以看到返回结果中,result 字段为 updated,即表示更新成功,另外, 字段 _version 代表更新后的版本号数,2 代表这是第二个版本,因为之前已经插入过一次数据,所以第一次插入的数据是版本 1,以后每更新一次,版本号都会加 1.

另外更新操作其实利用 index() 方法同样可以做到,写法如下:

es.index(index='news', doc_type='politics', body=data, id=1)

可以看到,index() 方法可以完成两个操作,如果数据不存在,那就执行插入操作,如果已经存在,那就执行更新操作,非常方便.

删除数据

如果想删除一条数据可以调用 delete() 方法,指定需要删除的数据 id 即可,写法如下:

运行结果如下:

{'_index': 'news', '_type': 'politics', '_id': '1', '_version': 3, 'result': 'deleted', '_shards': {'total': 2, 'successful': 1, 'failed': 0}, '_seq_no': 2, '_primary_term': 1}

运行结果中 result 字段为 deleted,代表删除成功,_version 变成了 3,又增加了 1

查询数据

Elasticsearch 更特殊的地方在于其异常强大的检索功能.

对于中文来说,我们需要安装一个分词插件,这里使用的是 elasticsearch-analysis-ik,GitHub 链接为:https://github.com/medcl/elasticsearch-analysis-ik

查看版本: https://github.com/medcl/elasticsearch-analysis-ik/releases

elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.8.8/elasticsearch-analysis-ik-6.8.8.zip

安装之后重新启动 Elasticsearch 就可以了,它会自动加载安装好的插件.

首先新建一个索引并指定需要分词的字段,代码如下:

from elasticsearch import Elasticsearch

es = Elasticsearch()
mapping = {
    'properties': {
        'title': {
            'type': 'text',
            'analyzer': 'ik_max_word',
            'search_analyzer': 'ik_max_word'
        }
    }
}
es.indices.delete(index='news', ignore=[400, 404])
es.indices.create(index='news', ignore=400)
result = es.indices.put_mapping(index='news', doc_type='politics', body=mapping)
print(result)

这里先将之前的索引删除了,然后新建了一个索引,然后更新了它的 mapping 信息,mapping 信息中指定了:

  • 分词的字段 ‘title’
  • 字段的类型 type 为 text,
  • 分词器 analyzer 和
  • 搜索分词器 search_analyzer 为 ik_max_word,即使用刚才安装的中文分词插件. 如果不指定的话则使用默认的英文分词器

接下来插入几条新的数据:

datas = [
    {
        'title': '美国留给伊拉克的是个烂摊子吗',
        'url': 'http://view.news.qq.com/zt2011/usa_iraq/index.htm',
        'date': '2011-12-16'
    },
    {
        'title': '公安部:各地校车将享最高路权',
        'url': 'http://www.chinanews.com/gn/2011/12-16/3536077.shtml',
        'date': '2011-12-16'
    },
    {
        'title': '中韩渔警冲突调查:韩警平均每天扣1艘中国渔船',
        'url': 'https://news.qq.com/a/20111216/001044.htm',
        'date': '2011-12-17'
    },
    {
        'title': '中国驻洛杉矶领事馆遭亚裔男子枪击 嫌犯已自首',
        'url': 'http://news.ifeng.com/world/detail_2011_12/16/11372558_0.shtml',
        'date': '2011-12-18'
    }
]

for data in datas:
    es.index(index='news', doc_type='politics', body=data)

这里指定了四条数据,都带有 title、url、date 字段,然后通过 index() 方法将其插入 Elasticsearch 中,索引名称为 news,类型为 politics.

根据关键词查询一下相关内容:

result = es.search(index='news', doc_type='politics')
print(result)
'''
{
   "took":68,
   "timed_out":False,
   "_shards":{
      "total":5,
      "successful":5,
      "skipped":0,
      "failed":0
   },
   "hits":{
      "total":4,
      "max_score":1.0,
      "hits":[
         {
            "_index":"news",
            "_type":"politics",
            "_id":"uLz5A3IBtq_Rh7vrXAyj",
            "_score":1.0,
            "_source":{
               "title":"美国留给伊拉克的是个烂摊子吗",
               "url":"http://view.news.qq.com/zt2011/usa_iraq/index.htm",
               "date":"2011-12-16"
            }
         },
         {
            "_index":"news",
            "_type":"politics",
            "_id":"ubz5A3IBtq_Rh7vrXQxC",
            "_score":1.0,
            "_source":{
               "title":"公安部:各地校车将享最高路权",
               "url":"http://www.chinanews.com/gn/2011/12-16/3536077.shtml",
               "date":"2011-12-16"
            }
         },
         {
            "_index":"news",
            "_type":"politics",
            "_id":"urz5A3IBtq_Rh7vrXQxJ",
            "_score":1.0,
            "_source":{
               "title":"中韩渔警冲突调查:韩警平均每天扣1艘中国渔船",
               "url":"https://news.qq.com/a/20111216/001044.htm",
               "date":"2011-12-17"
            }
         },
         {
            "_index":"news",
            "_type":"politics",
            "_id":"u7z5A3IBtq_Rh7vrXQxT",
            "_score":1.0,
            "_source":{
               "title":"中国驻洛杉矶领事馆遭亚裔男子枪击 嫌犯已自首",
               "url":"http://news.ifeng.com/world/detail_2011_12/16/11372558_0.shtml",
               "date":"2011-12-18"
            }
         }
      ]
   }
}
'''

返回结果会出现在 hits 字段里面,然后其中有 total 字段标明了查询的结果条目数,还有 max_score 代表了最大匹配分数

还可以进行全文检索,这才是体现 Elasticsearch 搜索引擎特性的地方:

import json

dsl = {
    'query': {
        'match': {
            'title': '中国 领事馆'
        }
    }
}

es = Elasticsearch()
result = es.search(index='news', doc_type='politics', body=dsl)
print(json.dumps(result, indent=2, ensure_ascii=False))

这里使用 Elasticsearch 支持的 DSL 语句来进行查询,使用 match 指定全文检索,检索的字段是 title,内容是“中国领事馆”,搜索结果如下:

{
  "took": 27,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 2,
    "max_score": 2.2303011,
    "hits": [
      {
        "_index": "news",
        "_type": "politics",
        "_id": "u7z5A3IBtq_Rh7vrXQxT",
        "_score": 2.2303011,
        "_source": {
          "title": "中国驻洛杉矶领事馆遭亚裔男子枪击 嫌犯已自首",
          "url": "http://news.ifeng.com/world/detail_2011_12/16/11372558_0.shtml",
          "date": "2011-12-18"
        }
      },
      {
        "_index": "news",
        "_type": "politics",
        "_id": "urz5A3IBtq_Rh7vrXQxJ",
        "_score": 0.18493031,
        "_source": {
          "title": "中韩渔警冲突调查:韩警平均每天扣1艘中国渔船",
          "url": "https://news.qq.com/a/20111216/001044.htm",
          "date": "2011-12-17"
        }
      }
    ]
  }
}

Process finished with exit code 0

这里可以看到匹配的结果有两条,第一条的分数为 2.23,第二条的分数为 0.18,这是因为第一条匹配的数据中含有“中国”和“领事馆”两个词,第二条匹配的数据中不包含“领事馆”,但是包含了“中国”这个词,所以也被检索出来了,但是分数比较低.

因此可以看出,检索时会对对应的字段全文检索,结果还会按照检索关键词的相关性进行排序,这就是一个基本的搜索引擎雏形

倒排索引原理

倒排索引是目前搜索引擎公司对搜索引擎最常用的存储方式,也是搜索引擎的核心内容,在搜索引擎的实际应用中,有时需要按照关键字的某些值查找记录,所以是按照关键字建立索引,这个索引就被称为倒排索引.

首先, 索引一般是用于提高查询效率的. 举个最简单的例子,已知有5个文本文件,需要查某个单词位于哪个文本文件中,最直观的做法就是逐个加载每个文本文件中的单词到内存中,然后用for循环遍历一遍数组,直到找到这个单词. 这种做法就是正向索引的思路(查询效率极低).

倒排索引的例子:

有两段文本

D1:Hello, conan!

D2:Hello, hattori!
  1. 找到所有的单词
    • Hello、conan、hattori
  2. 找到包含这些单词的文本位置
    • Hello(D1,D2)
    • conan(D1)
    • hattori(D2)
  3. 将单词作为Hash表的Key,将所在的文本位置作为Hash表的Value保存起来
  4. 当要查询某个单词的所在位置时,只需要根据这张Hash表就可以迅速的找到目标文档

正向索引是通过文档去查找单词,反向索引则是通过单词去查找文档.

优点: 在处理复杂的多关键字查询时,可在倒排表中先完成查询的并、交等逻辑运算,得到结果后再对记录进行存取,这样把对文档的查询转换为地址集合的运算,从而提高查找速度.

总结

倒排索引创建索引的流程:

  1. 首先把所有的原始数据进行编号,形成文档列表
  2. 把文档数据进行分词,得到很多的词条,以词条为索引。保存包含这些词条的文档的编号信息。

搜索的过程:当用户输入任意的词条时,首先对用户输入的数据进行分词,得到用户要搜索的所有词条,然后拿着这些词条去倒排索引列表中进行匹配。找到这些词条就能找到包含这些词条的所有文档的编号. 然后根据这些编号去文档列表中找到文档.

例子

Allrecipes(食譜网站)抓取數據並將其存儲在ES中. 需要創建一個嚴格的Schema或Mapping,以確保以正確的格式和類型對數據進行索引.

import json
import logging
from pprint import pprint
from time import sleep

import requests
from bs4 import BeautifulSoup
from elasticsearch import Elasticsearch

# 顯示查詢結果
def search(es_object, index_name, search):
    res = es_object.search(index=index_name, body=search)
    pprint(res)

# 創建Index(索引), 將索引命名為recipes,Type為salads
# 給文檔結構創建一個 mapping(映射)
def create_index(es_object, index_name):
    created = False
    # index settings
    # Mapping是Elastic的Schema術語, 包含整個文檔結構的映射
    # 就像其他數據庫Table表中設置某些字段數據類型
    settings = {
        "settings": {
            "number_of_shards": 1,
            "number_of_replicas": 0
        },
        "mappings": {
            "salads": {
                "dynamic": "strict",
                "properties": {
                    "title": {
                        "type": "text"
                    },
                    "submitter": {
                        "type": "text"
                    },
                    "description": {
                        "type": "text"
                    },
                    "calories": {
                        "type": "integer"
                    },
                    "ingredients": {
                        "type": "nested",
                        "properties": {
                            "step": {"type": "text"}
                        }
                    },
                }
            }
        }
    }
    # 如果索引創建成功,則可以通過訪問http://localhost:9200/recipes/_mappings 進行驗證

    try:
        if not es_object.indices.exists(index_name):
            # Ignore 400 means to ignore "Index Already Exist" error.
            es_object.indices.create(index=index_name, ignore=400, body=settings)
            print('Created Index')
        created = True
    except Exception as ex:
        print(str(ex))
    finally:
        return created

# 存儲實際數據或文檔
def store_record(elastic_object, index_name, record):
    is_stored = True
    try:
        outcome = elastic_object.index(index=index_name, doc_type='salads', body=record)
        print(outcome)
    except Exception as ex:
        print('Error in indexing data')
        print(str(ex))
        is_stored = False
    finally:
        return is_stored


# 連接ElasticSearch服務器
def connect_elasticsearch():
    _es = None
    _es = Elasticsearch([{'host': 'localhost', 'port': 9200}])
    if _es.ping():  # _es.ping()對服務器發送ping請求,如果連接上的話返回True
        print('Yay Connected')
    else:
        print('Awww it could not connect!')
    return _es

# 爬取数据并进行转换, 最最终输出json格式的数据
def parse(u):
    title = '-'
    submit_by = '-'
    description = '-'
    calories = 0
    ingredients = []
    rec = {}

    try:
        r = requests.get(u, headers=headers)

        if r.status_code == 200:
            html = r.text
            soup = BeautifulSoup(html, 'lxml')
            # title
            title_section = soup.select('.recipe-summary__h1')
            # submitter
            submitter_section = soup.select('.submitter__name')
            # description
            description_section = soup.select('.submitter__description')
            # ingredients
            ingredients_section = soup.select('.recipe-ingred_txt')

            # calories
            calories_section = soup.select('.calorie-count')
            if calories_section:
                calories = calories_section[0].text.replace('cals', '').strip()

            if ingredients_section:
                for ingredient in ingredients_section:
                    ingredient_text = ingredient.text.strip()
                    if 'Add all ingredients to list' not in ingredient_text and ingredient_text != '':
                        ingredients.append({'step': ingredient.text.strip()})

            if description_section:
                description = description_section[0].text.strip().replace('"', '')

            if submitter_section:
                submit_by = submitter_section[0].text.strip()

            if title_section:
                title = title_section[0].text

            rec = {'title': title, 'submitter': submit_by, 'description': description, 'calories': calories,
                   'ingredients': ingredients}
    except Exception as ex:
        print('Exception while parsing')
        print(str(ex))
    finally:
        return json.dumps(rec)


if __name__ == '__main__':
    headers = {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36',
        'Pragma': 'no-cache'
    }
    logging.basicConfig(level=logging.ERROR)

    url = 'https://www.allrecipes.com/recipes/96/salad/'
    r = requests.get(url, headers=headers)
    if r.status_code == 200:
        html = r.text
        soup = BeautifulSoup(html, 'lxml')
        links = soup.select('.fixed-recipe-card__h3 a')
        if len(links) > 0:
            es = connect_elasticsearch()

        for link in links:
            sleep(2)
            result = parse(link['href'])
            if es is not None:
                if create_index(es, 'recipes'):
                    out = store_record(es, 'recipes', result)
                    print('Data indexed successfully')

    es = connect_elasticsearch()
    if es is not None:
        # search_object = {'query': {'match': {'calories': '102'}}}
        # search_object = {'_source': ['title'], 'query': {'match': {'calories': '102'}}}
        search_object = {'_source': ['title'], 'query': {'range': {'calories': {'gte': 20}}}}
        search(es, 'recipes', json.dumps(search_object))
        # 上面查詢將返回其中calories大于20的所有記錄

output:

Yay Connected
Created Index
{'_index': 'recipes', '_type': 'salads', '_id': 'vLwtBHIBtq_Rh7vr0gzX', '_version': 1, 'result': 'created', '_shards': {'total': 1, 'successful': 1, 'failed': 0}, '_seq_no': 0, '_primary_term': 1}
Data indexed successfully
{'_index': 'recipes', '_type': 'salads', '_id': 'vbwtBHIBtq_Rh7vr4AyL', '_version': 1, 'result': 'created', '_shards': {'total': 1, 'successful': 1, 'failed': 0}, '_seq_no': 1, '_primary_term': 1}
Data indexed successfully
{'_index': 'recipes', '_type': 'salads', '_id': 'vrwtBHIBtq_Rh7vr7gxh', '_version': 1, 'result': 'created', '_shards': {'total': 1, 'successful': 1, 'failed': 0}, '_seq_no': 2, '_primary_term': 1}
Data indexed successfully
{'_index': 'recipes', '_type': 'salads', '_id': 'v7wtBHIBtq_Rh7vr_gyf', '_version': 1, 'result': 'created', '_shards': {'total': 1, 'successful': 1, 'failed': 0}, '_seq_no': 3, '_primary_term': 1}
Data indexed successfully
{'_index': 'recipes', '_type': 'salads', '_id': 'wLwuBHIBtq_Rh7vrCwwH', '_version': 1, 'result': 'created', '_shards': {'total': 1, 'successful': 1, 'failed': 0}, '_seq_no': 4, '_primary_term': 1}
Data indexed successfully
{'_index': 'recipes', '_type': 'salads', '_id': 'wbwuBHIBtq_Rh7vrIQw-', '_version': 1, 'result': 'created', '_shards': {'total': 1, 'successful': 1, 'failed': 0}, '_seq_no': 5, '_primary_term': 1}
Data indexed successfully
{'_index': 'recipes', '_type': 'salads', '_id': 'wrwuBHIBtq_Rh7vrLwzL', '_version': 1, 'result': 'created', '_shards': {'total': 1, 'successful': 1, 'failed': 0}, '_seq_no': 6, '_primary_term': 1}
Data indexed successfully
{'_index': 'recipes', '_type': 'salads', '_id': 'w7wuBHIBtq_Rh7vrPQwY', '_version': 1, 'result': 'created', '_shards': {'total': 1, 'successful': 1, 'failed': 0}, '_seq_no': 7, '_primary_term': 1}
Data indexed successfully
{'_index': 'recipes', '_type': 'salads', '_id': 'xLwuBHIBtq_Rh7vrSQzY', '_version': 1, 'result': 'created', '_shards': {'total': 1, 'successful': 1, 'failed': 0}, '_seq_no': 8, '_primary_term': 1}
Data indexed successfully
{'_index': 'recipes', '_type': 'salads', '_id': 'xbwuBHIBtq_Rh7vrVgx1', '_version': 1, 'result': 'created', '_shards': {'total': 1, 'successful': 1, 'failed': 0}, '_seq_no': 9, '_primary_term': 1}
Data indexed successfully
{'_index': 'recipes', '_type': 'salads', '_id': 'xrwuBHIBtq_Rh7vrYwxh', '_version': 1, 'result': 'created', '_shards': {'total': 1, 'successful': 1, 'failed': 0}, '_seq_no': 10, '_primary_term': 1}
Data indexed successfully
{'_index': 'recipes', '_type': 'salads', '_id': 'x7wuBHIBtq_Rh7vrcAwi', '_version': 1, 'result': 'created', '_shards': {'total': 1, 'successful': 1, 'failed': 0}, '_seq_no': 11, '_primary_term': 1}
Data indexed successfully
{'_index': 'recipes', '_type': 'salads', '_id': 'yLwuBHIBtq_Rh7vrfAz6', '_version': 1, 'result': 'created', '_shards': {'total': 1, 'successful': 1, 'failed': 0}, '_seq_no': 12, '_primary_term': 1}
Data indexed successfully
{'_index': 'recipes', '_type': 'salads', '_id': 'ybwuBHIBtq_Rh7vriQy9', '_version': 1, 'result': 'created', '_shards': {'total': 1, 'successful': 1, 'failed': 0}, '_seq_no': 13, '_primary_term': 1}
Data indexed successfully
{'_index': 'recipes', '_type': 'salads', '_id': 'yrwuBHIBtq_Rh7vroAw_', '_version': 1, 'result': 'created', '_shards': {'total': 1, 'successful': 1, 'failed': 0}, '_seq_no': 14, '_primary_term': 1}
Data indexed successfully
{'_index': 'recipes', '_type': 'salads', '_id': 'y7wuBHIBtq_Rh7vrrgwo', '_version': 1, 'result': 'created', '_shards': {'total': 1, 'successful': 1, 'failed': 0}, '_seq_no': 15, '_primary_term': 1}
Data indexed successfully
{'_index': 'recipes', '_type': 'salads', '_id': 'zLwuBHIBtq_Rh7vrvww0', '_version': 1, 'result': 'created', '_shards': {'total': 1, 'successful': 1, 'failed': 0}, '_seq_no': 16, '_primary_term': 1}
Data indexed successfully
{'_index': 'recipes', '_type': 'salads', '_id': 'zbwuBHIBtq_Rh7vr0gyw', '_version': 1, 'result': 'created', '_shards': {'total': 1, 'successful': 1, 'failed': 0}, '_seq_no': 17, '_primary_term': 1}
Data indexed successfully
{'_index': 'recipes', '_type': 'salads', '_id': 'zrwuBHIBtq_Rh7vr3wyQ', '_version': 1, 'result': 'created', '_shards': {'total': 1, 'successful': 1, 'failed': 0}, '_seq_no': 18, '_primary_term': 1}
Data indexed successfully
{'_index': 'recipes', '_type': 'salads', '_id': 'z7wuBHIBtq_Rh7vr7Ayl', '_version': 1, 'result': 'created', '_shards': {'total': 1, 'successful': 1, 'failed': 0}, '_seq_no': 19, '_primary_term': 1}
Data indexed successfully
{'_index': 'recipes', '_type': 'salads', '_id': '0LwuBHIBtq_Rh7vr_QzZ', '_version': 1, 'result': 'created', '_shards': {'total': 1, 'successful': 1, 'failed': 0}, '_seq_no': 20, '_primary_term': 1}
Data indexed successfully
{'_index': 'recipes', '_type': 'salads', '_id': '0bwvBHIBtq_Rh7vrDwzk', '_version': 1, 'result': 'created', '_shards': {'total': 1, 'successful': 1, 'failed': 0}, '_seq_no': 21, '_primary_term': 1}
Data indexed successfully
{'_index': 'recipes', '_type': 'salads', '_id': '0rwvBHIBtq_Rh7vrJAxx', '_version': 1, 'result': 'created', '_shards': {'total': 1, 'successful': 1, 'failed': 0}, '_seq_no': 22, '_primary_term': 1}
Data indexed successfully
{'_index': 'recipes', '_type': 'salads', '_id': '07wvBHIBtq_Rh7vrPwzR', '_version': 1, 'result': 'created', '_shards': {'total': 1, 'successful': 1, 'failed': 0}, '_seq_no': 23, '_primary_term': 1}
Data indexed successfully
{'_index': 'recipes', '_type': 'salads', '_id': '1LwvBHIBtq_Rh7vrYgwW', '_version': 1, 'result': 'created', '_shards': {'total': 1, 'successful': 1, 'failed': 0}, '_seq_no': 24, '_primary_term': 1}
Data indexed successfully
{'_index': 'recipes', '_type': 'salads', '_id': '1bwvBHIBtq_Rh7vrgQxw', '_version': 1, 'result': 'created', '_shards': {'total': 1, 'successful': 1, 'failed': 0}, '_seq_no': 25, '_primary_term': 1}
Data indexed successfully
{'_index': 'recipes', '_type': 'salads', '_id': '1rwvBHIBtq_Rh7vrpAyE', '_version': 1, 'result': 'created', '_shards': {'total': 1, 'successful': 1, 'failed': 0}, '_seq_no': 26, '_primary_term': 1}
Data indexed successfully
{'_index': 'recipes', '_type': 'salads', '_id': '17wvBHIBtq_Rh7vrsQy0', '_version': 1, 'result': 'created', '_shards': {'total': 1, 'successful': 1, 'failed': 0}, '_seq_no': 27, '_primary_term': 1}
Data indexed successfully
Yay Connected
{'_shards': {'failed': 0, 'skipped': 0, 'successful': 1, 'total': 1},
 'hits': {'hits': [{'_id': 'w7wuBHIBtq_Rh7vrPQwY',
                    '_index': 'recipes',
                    '_score': 1.0,
                    '_source': {'title': 'Awesome Pasta Salad'},
                    '_type': 'salads'},
                   {'_id': 'xbwuBHIBtq_Rh7vrVgx1',
                    '_index': 'recipes',
                    '_score': 1.0,
                    '_source': {'title': "Jamie's Cranberry Spinach Salad"},
                    '_type': 'salads'},
                   {'_id': 'xrwuBHIBtq_Rh7vrYwxh',
                    '_index': 'recipes',
                    '_score': 1.0,
                    '_source': {'title': 'Sweet Restaurant Slaw'},
                    '_type': 'salads'},
                   {'_id': 'xLwuBHIBtq_Rh7vrSQzY',
                    '_index': 'recipes',
                    '_score': 1.0,
                    '_source': {'title': 'Mediterranean Greek Salad'},
                    '_type': 'salads'},
                   {'_id': 'vLwtBHIBtq_Rh7vr0gzX',
                    '_index': 'recipes',
                    '_score': 1.0,
                    '_source': {'title': 'Spinach and Strawberry Salad'},
                    '_type': 'salads'},
                   {'_id': 'vrwtBHIBtq_Rh7vr7gxh',
                    '_index': 'recipes',
                    '_score': 1.0,
                    '_source': {'title': 'Taco Slaw'},
                    '_type': 'salads'},
                   {'_id': 'vbwtBHIBtq_Rh7vr4AyL',
                    '_index': 'recipes',
                    '_score': 1.0,
                    '_source': {'title': 'Roasted Beet Salad '},
                    '_type': 'salads'},
                   {'_id': 'x7wuBHIBtq_Rh7vrcAwi',
                    '_index': 'recipes',
                    '_score': 1.0,
                    '_source': {'title': 'Strawberry Spinach Salad I'},
                    '_type': 'salads'},
                   {'_id': 'wLwuBHIBtq_Rh7vrCwwH',
                    '_index': 'recipes',
                    '_score': 1.0,
                    '_source': {'title': 'Freekeh Salad with Tahini Dressing'},
                    '_type': 'salads'},
                   {'_id': 'yLwuBHIBtq_Rh7vrfAz6',
                    '_index': 'recipes',
                    '_score': 1.0,
                    '_source': {'title': 'Mexican Bean Salad'},
                    '_type': 'salads'}],
          'max_score': 1.0,
          'total': 13},
 'timed_out': False,
 'took': 17}

Process finished with exit code 0

https://vimsky.com/zh-tw/article/4490.html

Search DSL

https://www.jianshu.com/p/462007422e65

Ref

https://cuiqingcai.com/6214.html https://elasticsearch-py.readthedocs.io/en/master/api.html