持久化
您可以使用 dsl 库来定义您的映射和应用程序的基本持久层。
有关更全面的示例,请查看存储库中的 examples 目录。
文档
如果您想围绕您的文档创建一个模型化的包装器,请使用 Document
类。它还可以用于在 Elasticsearch 中创建所有必要的映射和设置(有关详细信息,请参阅 文档生命周期)。
from datetime import datetime
from elasticsearch_dsl import Document, Date, Nested, Boolean, \
analyzer, InnerDoc, Completion, Keyword, Text
html_strip = analyzer('html_strip',
tokenizer="standard",
filter=["standard", "lowercase", "stop", "snowball"],
char_filter=["html_strip"]
)
class Comment(InnerDoc):
author = Text(fields={'raw': Keyword()})
content = Text(analyzer='snowball')
created_at = Date()
def age(self):
return datetime.now() - self.created_at
class Post(Document):
title = Text()
title_suggest = Completion()
created_at = Date()
published = Boolean()
category = Text(
analyzer=html_strip,
fields={'raw': Keyword()}
)
comments = Nested(Comment)
class Index:
name = 'blog'
def add_comment(self, author, content):
self.comments.append(
Comment(author=author, content=content, created_at=datetime.now()))
def save(self, ** kwargs):
self.created_at = datetime.now()
return super().save(** kwargs)
数据类型
Document
实例使用本机 Python 类型,如 str
和 datetime
。在 Object
或 Nested
字段的情况下,将使用 InnerDoc
子类的实例,如上面示例中的 add_comment
方法,我们在这里创建了 Comment
类的实例。
有一些特定类型是作为此库的一部分创建的,以使使用某些字段类型更容易,例如在任何 范围字段 中使用的 Range
对象。
from elasticsearch_dsl import Document, DateRange, Keyword, Range
class RoomBooking(Document):
room = Keyword()
dates = DateRange()
rb = RoomBooking(
room='Conference Room II',
dates=Range(
gte=datetime(2018, 11, 17, 9, 0, 0),
lt=datetime(2018, 11, 17, 10, 0, 0)
)
)
# Range supports the in operator correctly:
datetime(2018, 11, 17, 9, 30, 0) in rb.dates # True
# you can also get the limits and whether they are inclusive or exclusive:
rb.dates.lower # datetime(2018, 11, 17, 9, 0, 0), True
rb.dates.upper # datetime(2018, 11, 17, 10, 0, 0), False
# empty range is unbounded
Range().lower # None, False
Python 类型提示
如果需要,可以使用标准 Python 类型提示来定义文档字段。以下是一些简单的示例
from typing import Optional
class Post(Document):
title: str # same as title = Text(required=True)
created_at: Optional[datetime] # same as created_at = Date(required=False)
published: bool # same as published = Boolean(required=True)
重要的是要注意,当使用 Field
子类(如 Text
、Date
和 Boolean
)时,必须将它们放在赋值的右侧,如上面的示例所示。将这些类用作类型提示会导致错误。
Python 类型根据以下表格映射到其相应的字段类型
Python 类型 |
DSL 字段 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
要将字段类型为可选,可以使用 Python typing
包中的标准 Optional
修饰符。可以将 List
修饰符添加到字段以将其转换为数组,类似于在字段对象上使用 multi=True
参数。
from typing import Optional, List
class MyDoc(Document):
pub_date: Optional[datetime] # same as pub_date = Date()
authors: List[str] # same as authors = Text(multi=True, required=True)
comments: Optional[List[str]] # same as comments = Text(multi=True)
还可以为字段提供 InnerDoc
子类的类型提示,在这种情况下,它将成为该类的 Object
字段。当 InnerDoc
子类用 List
包装时,将创建一个 Nested
字段。
from typing import List
class Address(InnerDoc):
...
class Comment(InnerDoc):
...
class Post(Document):
address: Address # same as address = Object(Address, required=True)
comments: List[Comment] # same as comments = Nested(Comment, required=True)
不幸的是,不可能有 Python 类型提示来唯一标识每种可能的 Elasticsearch 字段类型。要选择与表格中不同的字段类型,可以在字段声明中将字段实例显式添加为右侧赋值。下一个示例创建一个类型为 Optional[str]
的字段,但映射到 Keyword
而不是 Text
class MyDocument(Document):
category: Optional[str] = Keyword()
此形式也可以在需要提供其他选项来初始化字段时使用,例如在使用自定义分析器设置或更改 required
默认值时
class Comment(InnerDoc):
content: str = Text(analyzer='snowball', required=True)
当使用上面的类型提示时,Document
和 InnerDoc
的子类继承了与 Python 数据类相关联的一些行为,如 PEP 681 和 dataclass_transform 装饰器 所定义。要添加每个字段的数据类选项(如 default
或 default_factory
),可以在类型化字段声明的右侧使用 mapped_field()
包装器
class MyDocument(Document):
title: str = mapped_field(default="no title")
created_at: datetime = mapped_field(default_factory=datetime.now)
published: bool = mapped_field(default=False)
category: str = mapped_field(Keyword(required=True), default="general")
当使用 mapped_field()
包装函数时,可以将显式字段类型实例作为第一个位置参数传递,如上面示例中的 category
字段。
静态类型检查器(如 mypy 和 pyright)可以使用类型提示和添加到 mapped_field()
函数中的数据类特定选项来改进类型推断,并在 IDE 中提供更好的实时建议。
类型检查器无法推断出正确类型的一种情况是在使用字段作为类属性时。考虑以下示例
class MyDocument(Document):
title: str
doc = MyDocument()
# doc.title is typed as "str" (correct)
# MyDocument.title is also typed as "str" (incorrect)
为了帮助类型检查器正确识别类属性,必须将 M
泛型用作类型提示的包装器,如以下示例所示
from elasticsearch_dsl import M
class MyDocument(Document):
title: M[str]
created_at: M[datetime] = mapped_field(default_factory=datetime.now)
doc = MyDocument()
# doc.title is typed as "str"
# doc.created_at is typed as "datetime"
# MyDocument.title is typed as "InstrumentedField"
# MyDocument.created_at is typed as "InstrumentedField"
请注意,M
类型提示不提供任何运行时行为,并且不需要使用它,但它可能有助于消除 IDE 或类型检查构建中的虚假类型错误。
当字段作为类属性访问时返回的 InstrumentedField
对象是字段实例的代理,可以在需要引用字段的任何地方使用,例如在 Search
对象中指定排序选项时
# sort by creation date descending, and title ascending
s = MyDocument.search().sort(-MyDocument.created_at, MyDocument.title)
在指定排序顺序时,可以在类字段属性上使用 +
和 -
一元运算符来指示升序和降序。
关于日期的说明
elasticsearch-dsl
将始终尊重传递到 Elasticsearch 中或存储在 Elasticsearch 中的 datetime
对象上的时区信息(或缺乏时区信息)。Elasticsearch 本身将所有没有时区信息的日期时间解释为 UTC
。如果您希望在 Python 代码中反映这一点,可以在实例化 Date
字段时指定 default_timezone
class Post(Document):
created_at = Date(default_timezone='UTC')
在这种情况下,传递的任何 datetime
对象(或从 Elasticsearch 解析的任何 datetime
对象)将被视为在 UTC
时区中。
文档生命周期
在您首次使用 Post
文档类型之前,您需要在 Elasticsearch 中创建映射。为此,您可以使用 Index 对象,也可以通过调用 init
类方法来直接创建映射
# create the mappings in Elasticsearch
Post.init()
此代码通常在代码部署期间在应用程序的设置中运行,类似于运行数据库迁移。
要创建一个新的 Post
文档,只需实例化该类并传入您希望设置的任何字段,然后您可以使用标准属性设置来更改/添加更多字段。请注意,您不限于显式定义的字段
# instantiate the document
first = Post(title='My First Blog Post, yay!', published=True)
# assign some field values, can be values or lists of values
first.category = ['everything', 'nothing']
# every document has an id in meta
first.meta.id = 47
# save the document into the cluster
first.save()
所有元数据字段(id
、routing
、index
等)可以通过 meta
属性或直接使用带下划线的变体来访问(和设置)
post = Post(meta={'id': 42})
# prints 42
print(post.meta.id)
# override default index
post.meta.index = 'my-blog'
注意
通过 meta
访问所有元数据意味着此名称是保留的,并且您不应该在文档上有一个名为 meta
的字段。但是,如果您需要它,您仍然可以使用获取项目(而不是属性)语法来访问数据:post['meta']
。
要检索现有文档,请使用 get
类方法
# retrieve the document
first = Post.get(id=42)
# now we can call methods, change fields, ...
first.add_comment('me', 'This is nice!')
# and save the changes into the cluster again
first.save()
还可以通过 update
方法使用 更新 API。默认情况下,除了 API 的参数之外的任何关键字参数都将被视为具有新值的字段。这些字段将在文档的本地副本上更新,然后作为部分文档发送以进行更新
# retrieve the document
first = Post.get(id=42)
# you can update just individual fields which will call the update API
# and also update the document in place
first.update(published=True, published_by='me')
如果您希望使用 painless
脚本执行更新,您可以将脚本字符串作为 script
传递,或者将 存储脚本 的 id
通过 script_id
传递。然后,update
方法的所有其他关键字参数将作为脚本的参数传递。文档不会就地更新。
# retrieve the document
first = Post.get(id=42)
# we execute a script in elasticsearch with additional kwargs being passed
# as params into the script
first.update(script='ctx._source.category.add(params.new_category)',
new_category='testing')
如果在 Elasticsearch 中找不到文档,将引发异常(elasticsearch.NotFoundError
)。如果你希望返回 None
而不是引发异常,只需传递 ignore=404
来抑制异常。
p = Post.get(id='not-in-es', ignore=404)
p is None
当你希望通过其 id
同时检索多个文档时,可以使用 mget
方法。
posts = Post.mget([42, 47, 256])
mget
默认情况下,如果任何文档未找到,将引发 NotFoundError
,如果任何文档导致错误,将引发 RequestError
。你可以通过设置参数来控制此行为。
raise_on_error
如果为
True
(默认),则任何错误都将导致引发异常。否则,所有包含错误的文档将被视为缺失。missing
可以有三个可能的值:
'none'
(默认)、'raise'
和'skip'
。如果文档缺失或出错,它将被替换为None
,引发异常或完全跳过输出列表中的文档。
与 Document
关联的索引可以通过 _index
类属性访问,该属性使你能够访问 Index 类。
_index
属性也是 load_mappings
方法的所在地,该方法将从 Elasticsearch 更新 Index
上的映射。如果你使用动态映射并希望类了解这些字段(例如,如果你希望 Date
字段被正确地(反)序列化),这非常有用。
Post._index.load_mappings()
要删除文档,只需调用其 delete
方法。
first = Post.get(id=42)
first.delete()
分析
要为 Text
字段指定 analyzer
值,你可以只使用分析器的名称(作为字符串),并依赖于已定义的分析器(如内置分析器)或手动定义分析器。
或者,你可以创建自己的分析器,并让持久层处理其创建,从我们之前的示例中。
from elasticsearch_dsl import analyzer, tokenizer
my_analyzer = analyzer('my_analyzer',
tokenizer=tokenizer('trigram', 'nGram', min_gram=3, max_gram=3),
filter=['lowercase']
)
每个分析对象都需要有一个名称(my_analyzer
和 trigram
在我们的示例中),分词器、分词过滤器和字符过滤器也需要指定类型(nGram
在我们的示例中)。
一旦你拥有自定义 analyzer
的实例,你也可以通过使用 simulate
方法在它上面调用 analyze API。
response = my_analyzer.simulate('Hello World!')
# ['hel', 'ell', 'llo', 'lo ', 'o w', ' wo', 'wor', 'orl', 'rld', 'ld!']
tokens = [t.token for t in response.tokens]
注意
在创建依赖于自定义分析器的映射时,索引必须不存在或已关闭。要创建多个 Document
定义的映射,可以使用 Index 对象。
搜索
要搜索此文档类型,请使用 search
类方法。
# by calling .search we get back a standard Search object
s = Post.search()
# the search is already limited to the index and doc_type of our document
s = s.filter('term', published=True).query('match', title='first')
results = s.execute()
# when you execute the search the results are wrapped in your document class (Post)
for post in results:
print(post.meta.score, post.title)
或者,你可以只获取一个 Search
对象,并将其限制为返回我们的文档类型,包装在正确的类中。
s = Search()
s = s.doc_type(Post)
你还可以将文档类与标准文档类型(仅字符串)组合,它们将像以前一样被处理。你还可以传入多个 Document
子类,响应中的每个文档都将包装在其类中。
如果你想运行建议,只需在 Search
对象上使用 suggest
方法。
s = Post.search()
s = s.suggest('title_suggestions', 'pyth', completion={'field': 'title_suggest'})
response = s.execute()
for result in response.suggest.title_suggestions:
print('Suggestions for %s:' % result.text)
for option in result.options:
print(' %s (%r)' % (option.text, option.payload))
class Meta
选项
在文档定义中的 Meta
类中,你可以为文档定义各种元数据。
mapping
Mapping
类的可选实例,用作从文档类本身的字段创建的映射的基础。
在 Meta
类上的任何属性,如果它们是 MetaField
的实例,将用于控制元字段的映射(_all
、dynamic
等)。只需将参数(不带前导下划线)命名为要映射的字段,并将任何参数传递给 MetaField
类。
class Post(Document):
title = Text()
class Meta:
all = MetaField(enabled=False)
dynamic = MetaField('strict')
class Index
选项
此部分的 Document
定义可以包含有关索引、其名称、设置和其他属性的任何信息。
name
要使用的索引的名称,如果它包含通配符(
*
),则它不能用于任何写入操作,并且必须在调用.save()
等方法时显式传递index
关键字参数。using
要使用的默认连接别名,默认为
'default'
。settings
包含
Index
对象的任何设置的字典,如number_of_shards
。analyzers
应该在索引上定义的分析器的附加列表(有关详细信息,请参阅 Analysis)。
aliases
包含任何别名定义的字典。
文档继承
你可以使用标准的 Python 继承来扩展模型,这在一些场景中非常有用。例如,如果你想有一个 BaseDocument
定义一些公共字段,这些字段应该被多个不同的 Document
类共享。
class User(InnerDoc):
username = Text(fields={'keyword': Keyword()})
email = Text()
class BaseDocument(Document):
created_by = Object(User)
created_date = Date()
last_updated = Date()
def save(**kwargs):
if not self.created_date:
self.created_date = datetime.now()
self.last_updated = datetime.now()
return super(BaseDocument, self).save(**kwargs)
class BlogPost(BaseDocument):
class Index:
name = 'blog'
另一个用例是使用 join 类型 在单个索引中拥有多个不同的实体。你可以看到 示例 的这种方法。请注意,在这种情况下,如果子类没有定义自己的 Index 类,则映射将合并并在所有子类之间共享。
Index
在典型的场景中,在 Document
类上使用 class Index
足以执行任何操作。但在一些情况下,直接操作 Index
对象可能很有用。
Index
是一个类,负责保存与 Elasticsearch 中索引相关的所有元数据 - 映射和设置。它在定义映射时最有用,因为它允许轻松地同时创建多个映射。这在迁移中设置 Elasticsearch 对象时特别有用。
from elasticsearch_dsl import Index, Document, Text, analyzer
blogs = Index('blogs')
# define custom settings
blogs.settings(
number_of_shards=1,
number_of_replicas=0
)
# define aliases
blogs.aliases(
old_blogs={}
)
# register a document with the index
blogs.document(Post)
# can also be used as class decorator when defining the Document
@blogs.document
class Post(Document):
title = Text()
# You can attach custom analyzers to the index
html_strip = analyzer('html_strip',
tokenizer="standard",
filter=["standard", "lowercase", "stop", "snowball"],
char_filter=["html_strip"]
)
blogs.analyzer(html_strip)
# delete the index, ignore if it doesn't exist
blogs.delete(ignore=404)
# create the index in elasticsearch
blogs.create()
你还可以为索引设置模板,并使用 clone
方法创建特定副本。
blogs = Index('blogs', using='production')
blogs.settings(number_of_shards=2)
blogs.document(Post)
# create a copy of the index with different name
company_blogs = blogs.clone('company-blogs')
# create a different copy on different cluster
dev_blogs = blogs.clone('blogs', using='dev')
# and change its settings
dev_blogs.setting(number_of_shards=1)
IndexTemplate
elasticsearch-dsl
还提供了一个选项,可以使用 IndexTemplate
类管理 Elasticsearch 中的 索引模板,该类与 Index
具有非常相似的 API。
一旦索引模板保存在 Elasticsearch 中,其内容将自动应用于与模板模式匹配的新索引(现有索引完全不受模板影响)(在我们的示例中,任何以 blogs-
开头的索引),即使索引是在将文档索引到该索引时自动创建的。
由单个模板控制的一组基于时间的索引的潜在工作流程。
from datetime import datetime
from elasticsearch_dsl import Document, Date, Text
class Log(Document):
content = Text()
timestamp = Date()
class Index:
name = "logs-*"
settings = {
"number_of_shards": 2
}
def save(self, **kwargs):
# assign now if no timestamp given
if not self.timestamp:
self.timestamp = datetime.now()
# override the index to go to the proper timeslot
kwargs['index'] = self.timestamp.strftime('logs-%Y%m%d')
return super().save(**kwargs)
# once, as part of application setup, during deploy/migrations:
logs = Log._index.as_template('logs', order=0)
logs.save()
# to perform search across all logs:
search = Log.search()