{"id":13783219,"url":"https://github.com/wallneradam/esorm","last_synced_at":"2025-04-07T06:05:33.274Z","repository":{"id":204730146,"uuid":"712015625","full_name":"wallneradam/esorm","owner":"wallneradam","description":"Python ElasticSearch ORM based on Pydantic","archived":false,"fork":false,"pushed_at":"2024-11-23T10:57:16.000Z","size":275,"stargazers_count":53,"open_issues_count":1,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-31T05:03:06.869Z","etag":null,"topics":["async","asyncio","database","elasticsearch","elasticsearch-client","fastapi","nosql","odm","orm","pydantic","pydantic-v2","python","python3"],"latest_commit_sha":null,"homepage":"https://esorm.readthedocs.io/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/wallneradam.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":["wallneradam"]}},"created_at":"2023-10-30T16:15:00.000Z","updated_at":"2025-03-21T04:58:06.000Z","dependencies_parsed_at":null,"dependency_job_id":"24b7ad65-a6e7-41ae-8125-9c0e805ee7bb","html_url":"https://github.com/wallneradam/esorm","commit_stats":null,"previous_names":["wallneradam/esorm"],"tags_count":27,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wallneradam%2Fesorm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wallneradam%2Fesorm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wallneradam%2Fesorm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wallneradam%2Fesorm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/wallneradam","download_url":"https://codeload.github.com/wallneradam/esorm/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247601447,"owners_count":20964864,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","asyncio","database","elasticsearch","elasticsearch-client","fastapi","nosql","odm","orm","pydantic","pydantic-v2","python","python3"],"created_at":"2024-08-03T19:00:16.461Z","updated_at":"2025-04-07T06:05:33.254Z","avatar_url":"https://github.com/wallneradam.png","language":"Python","funding_links":["https://github.com/sponsors/wallneradam"],"categories":["Elasticsearch developer tools and utilities"],"sub_categories":["Clients"],"readme":"\u003cimg src=\"https://raw.githubusercontent.com/wallneradam/esorm/main/docs/_static/img/esorm.svg\" width=\"110\" height=\"110\" align=\"left\" style=\"margin-right: 1em; margin-bottom: 0.5em\" alt=\"Logo\"/\u003e\n\n# ESORM - Python ElasticSearch ORM based on Pydantic\n\nESORM is an ElasticSearch Object Relational Mapper or Object Document Mapper (ODM) if you like,\n for Python based on Pydantic. It is a high-level library for managing ElasticSearch documents\n in Python. It is fully async and uses annotations and type hints for type checking and IDE autocompletion.\n    \n## ☰ Table of Contents\n\n- [💾 Installation](#installation)\n- [🚀 Features](#features)\n    - [Supported ElasticSearch versions](#supported-elasticsearch-versions)\n    - [Supported Python versions](#supported-python-versions)\n- [📖 Usage](#usage)\n    - [Define a model](#define-a-model)\n        - [Python basic types](#python-basic-types)\n        - [ESORM field types](#esorm-field-types)\n        - [Nested documents](#nested-documents)\n        - [List primitive fields](#list-primitive-fields)\n        - [ESBaseModel](#esbasemodel)\n        - [Id field](#id-field)\n        - [Model Settings](#model-settings)\n        - [Describe fields](#describe-fields)\n        - [ESModelTimestamp](#esmodeltimestamp)\n    - [Connecting to ElasticSearch](#connecting-to-elasticsearch)\n        - [Client](#client)\n    - [Create index templates](#create-index-templates)\n    - [Create indices and mappings](#create-indices-and-mappings)\n    - [Model instances](#model-instances)\n    - [CRUD: Create](#crud-create)\n    - [CRUD: Read](#crud-read)\n    - [CRUD: Update](#crud-update)\n    - [CRUD: Delete](#crud-delete)\n    - [Bulk operations](#bulk-operations)\n    - [Search](#search)\n        - [General search](#general-search)\n        - [Search with field value terms (dictioanry search)](#search-with-field-value-terms-dictionary-search)\n    - [Aggregations](#aggregations)\n    - [Pagination and sorting](#pagination-and-sorting)\n- [🔬 Advanced usage](docs/advanced.md#advanced-usage) \n    - [Optimistic concurrency control](docs/advanced.md#optimistic-concurrency-control)\n    - [Lazy properties](docs/advanced.md#lazy-properties)\n    - [Shard routing](docs/advanced.md#shard-routing)\n    - [Retreive Selected Fields Only](docs/advanced.md#retreive-selected-fields-only)\n    - [Watchers](docs/advanced.md#watchers)\n    - [FastAPI integration](docs/advanced.md#fastapi-integration)\n- [🧪 Testing](#testing)\n- [🛡 License](#license)\n- [📃 Citation](#citation)\n\n\u003ca id=\"installation\"\u003e\u003c/a\u003e\n## 💾 Installation\n\n\n```bash\npip install pyesorm\n```\n\n\u003ca id=\"features\"\u003e\u003c/a\u003e\n## 🚀 Features\n\n- Pydantic model representation of ElasticSearch documents\n- Automatic mapping and index creation\n- CRUD operations\n- Full async support (no sync version at all)\n- Mapping to and from ElasticSearch types\n- Support for nested documents\n- Automatic optimistic concurrency control\n- Custom id field\n- Context for bulk operations\n- Supported IDE autocompletion and type checking (PyCharm tested)\n- Everything in the source code is documented and annotated\n- `TypedDict`s for ElasticSearch queries and aggregations\n- Docstring support for fields\n- Shard routing support\n- Lazy properties\n- Support \u003e= Python 3.8 (tested with 3.8 through 3.12)\n- Support for ElasticSearch 8.x and 7.x\n- Watcher support (You may need ElasticSearch subscription license for this)\n- Pagination and sorting\n- FastAPI integration\n\nNot all ElasticSearch features are supported yet, pull requests are welcome.\n\n\u003ca id=\"supported-elasticsearch-versions\"\u003e\u003c/a\u003e\n### Supported ElasticSearch versions\n\nIt is tested with ElasticSearch 7.x and 8.x.\n\n\u003ca id=\"supported-python-versions\"\u003e\u003c/a\u003e\n### Supported Python versions\n\nTested with Python 3.8 through 3.12.\n\n\u003ca id=\"usage\"\u003e\u003c/a\u003e\n## 📖 Usage\n\n\u003ca id=\"define-a-model\"\u003e\u003c/a\u003e\n### Define a model\n\nYou can use all [Pydantic](https://pydantic-docs.helpmanual.io/usage/models/) model features, because `ESModel` is a subclass of `pydantic.BaseModel`.\n(Actually it is a subclass of `ESBaseModel`, see more [below...](#esbasemodel))\n\n`ESModel` extends pydantic `BaseModel` with ElasticSearch specific features. It serializes and deserializes\ndocuments to and from ElasticSearch types and handle ElasticSearch operations in the background.\n\n\u003ca id=\"python-basic-types\"\u003e\u003c/a\u003e\n#### Python basic types\n\n```python\nfrom esorm import ESModel\n\n\nclass User(ESModel):\n    name: str\n    age: int\n```\n\nThis is how the python types are converted to ES types:\n\n| Python type         | ES type   | Comment                     |\n|---------------------|-----------|-----------------------------|\n| `str`               | `text`    |                             |\n| `int`               | `long`    |                             |\n| `float`             | `double`  |                             |\n| `bool`              | `boolean` |                             |\n| `datetime.datetime` | `date`    |                             |\n| `datetime.date`     | `date`    |                             |\n| `datetime.time`     | `date`    | Stored as 1970-01-01 + time |\n| `typing.Literal`    | `keyword` |                             |\n| `UUID`              | `keyword` |                             |\n| `Path`              | `keyword` |                             |\n| `IntEnum`           | `integer` |                             |\n| `Enum`              | `keyword` | also StrEnum                |\n\nSome special pydanctic types are also supported:\n\n| Pydantic type   | ES type   | Comment |\n|-----------------|-----------|---------|\n| `URL`           | `keyword` |         |\n| `IPvAddressAny` | `ip`      |         |\n\n\n\u003ca id=\"esorm-field-types\"\u003e\u003c/a\u003e\n#### ESORM field types\n\nYou can specify ElasticSearch special fields using `esorm.fields` module.\n\n```python\nfrom esorm import ESModel\nfrom esorm.fields import keyword, text, byte, geo_point\n\n\nclass User(ESModel):\n    name: text\n    email: keyword\n    age: byte\n    location: geo_point\n    ...\n```\n\nThe supported fields are:\n\n| Field name                  | ES type         |\n|-----------------------------|-----------------|\n| `keyword`                   | `keyword`       |\n| `text`                      | `text`          |\n| `binary`                    | `binary`        |\n| `byte`                      | `byte`          |\n| `short`                     | `short`         |\n| `integer` or `int32`        | `integer`       |\n| `long` or `int64`           | `long`          |\n| `unsigned_long` or `uint64` | `unsigned_long` |\n| `float16` or `half_float`   | `half_float`    |\n| `float32`                   | `float`         |\n| `double`                    | `double`        |\n| `boolean`                   | `boolean`       |\n| `geo_point`                 | `geo_point`     |\n\nThe `binary` field accepts **base64** encoded strings. However, if you provide `bytes` to it, they \nwill be automatically converted to a **base64** string during serialization. When you retrieve the \nfield, it will always be a **base64** encoded string. You can easily convert it back to bytes using \nthe `bytes()` method: `binary_field.bytes()`.\n\nYou can also use `Annotated` types to specify the ES type, like Pydantic `PositiveInt` and \n`NegativeInt` and similar.\n\n##### geo_point\n\nYou can use geo_point field type for location data:\n\n```python\nfrom esorm import ESModel\nfrom esorm.fields import geo_point\n\n\nclass Place(ESModel):\n    name: str\n    location: geo_point\n    \n\ndef create_place():\n    place = Place(name='Budapest', location=geo_point(lat=47.4979, long=19.0402))\n    place.save()\n```\n\n\n\u003ca id=\"nested-documents\"\u003e\u003c/a\u003e\n#### Nested documents\n\n```python\nfrom esorm import ESModel\nfrom esorm.fields import keyword, text, byte\n\n\nclass User(ESModel):\n    name: text\n    email: keyword\n    age: byte = 18\n\n\nclass Post(ESModel):\n    title: text\n    content: text\n    writer: User  # User is a nested document\n```\n\n\u003ca id=\"list-primitive-fields\"\u003e\u003c/a\u003e\n#### List primitive fields\n\nYou can use list of primitive fields:\n\n```python    \nfrom typing import List\nfrom esorm import ESModel\n\n\nclass User(ESModel):\n    emails: List[str]\n    favorite_ids: List[int] \n    ...   \n```\n\n\u003ca id=\"esbasemodel\"\u003e\u003c/a\u003e\n#### ESBaseModel\n\n`ESBaseModel` is the base of `ESModel`.\n\n##### Use it for abstract models\n\n```python\nfrom esorm import ESModel, ESBaseModel\nfrom esorm.fields import keyword, text, byte\n\n\n# This way `User` model won't be in the index\nclass BaseUser(ESBaseModel):  # \u003c---------------\n    # This config will be inherited by User\n    class ESConfig:\n        id_field = 'email'    \n    \n    name: text\n    email: keyword\n    \n\n# This will be in the index because it is a subclass of ESModel\nclass UserExtended(BaseUser, ESModel):\n    age: byte = 18\n\n\nasync def create_user():\n    user = UserExtended(\n        name='John Doe',\n        email=\"john@example.com\",\n        age=25\n    )\n    await user.save()\n```\n\n##### Use it for nested documents \n\nIt is useful to use it for nested documents, because by using it will not be included in the \nElasticSearch index.\n\n```python\nfrom esorm import ESModel, ESBaseModel\nfrom esorm.fields import keyword, text, byte\n\n\n# This way `User` model won't be in the index\nclass User(ESBaseModel):  # \u003c---------------\n    name: text\n    email: keyword\n    age: byte = 18\n\n\nclass Post(ESModel):\n    title: text\n    content: text\n    writer: User  # User is a nested document\n\n```  \n\n\u003ca id=\"id-field\"\u003e\u003c/a\u003e\n#### Id field\n\nYou can specify id field in [model settings](#model-settings):\n\n```python\nfrom esorm import ESModel\nfrom esorm.fields import keyword, text, byte\n\n\nclass User(ESModel):\n    class ESConfig:\n        id_field = 'email'\n\n    name: text\n    email: keyword\n    age: byte = 18\n```\n\nThis way the field specified in `id_field` will be removed from the document and used as the document `_id` in the\nindex.\n\nIf you specify a field named `id` in your model, it will be used as the document `_id` in the index\n(it will automatically override the `id_field` setting):\n\n```python\nfrom esorm import ESModel\n\n\nclass User(ESModel):\n    id: int  # This will be used as the document _id in the index\n    name: str\n```\n\nYou can also create an `__id__` property in your model to return a custom id:\n\n```python\nfrom esorm import ESModel\nfrom esorm.fields import keyword, text, byte\n\n\nclass User(ESModel):\n    name: text\n    email: keyword\n    age: byte = 18\n\n    @property\n    def __id__(self) -\u003e str:\n        return self.email\n```\n\nNOTE: annotation of `__id__` method is important, and it must be declared as a property.\n\n\u003ca id=\"model-settings\"\u003e\u003c/a\u003e\n#### Model Settings\n\nYou can specify model settings using `ESConfig` child class.\n\n```python\nfrom typing import Optional, List, Dict, Any\nfrom esorm import ESModel\n\n\nclass User(ESModel):\n    class ESConfig:\n        \"\"\" ESModel Config \"\"\"\n        # The index name\n        index: Optional[str] = None\n        # The name of the 'id' field\n        id_field: Optional[str] = None\n        # Default sort\n        default_sort: Optional[List[Dict[str, Dict[str, str]]]] = None\n        # ElasticSearch index settings (https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html)\n        settings: Optional[Dict[str, Any]] = None\n        # Maximum recursion depth of lazy properties\n        lazy_property_max_recursion_depth: int = 1\n```\n\n\u003ca id=\"esmodeltimestamp\"\u003e\u003c/a\u003e\n#### ESModelTimestamp\n\nYou can use `ESModelTimestamp` class to add `created_at` and `updated_at` fields to your model:\n\n```python \nfrom esorm import ESModelTimestamp\n\n\nclass User(ESModelTimestamp):\n    name: str\n    age: int\n```\n\nThese fields will be automatically updated to the actual `datetime` when you create or update a document.\nThe `created_at` field will be set only when you create a document. The `updated_at` field will be set\nwhen you create or update a document.\n\n\u003ca id=\"describe-fields\"\u003e\u003c/a\u003e\n#### Describe fields\n\nYou can use the usual `Pydantic` field description, but you can also use docstrings like this:\n\n```python\nfrom esorm import ESModel\nfrom esorm.fields import TextField\n\n\nclass User(ESModel):\n    name: str = 'John Doe'\n    \"\"\" The name of the user \"\"\"\n    age: int = 18\n    \"\"\" The age of the user \"\"\"\n\n    # This is the usual Pydantic way, but I think docstrings are more intuitive and readable\n    address: str = TextField(description=\"The address of the user\")\n```\n\nThe documentation is usseful if you create an API and you want to generate documentation from the model.\nIt can be used in [FastAPI](https://fastapi.tiangolo.com/) for example.\n\n\u003ca id=\"aliases\"\u003e\u003c/a\u003e\n### Aliases\n\nYou can specify aliases for fields:\n\n```python\nfrom esorm import ESModel\nfrom esorm.fields import keyword, Field\n\n\nclass User(ESModel):\n    full_name: keyword = Field(alias='fullName')  # In ES `fullName` will be the field name\n```\n\nThis is good for renaming fields in the model without changing the ElasticSearch field name.\n\n\u003ca id=\"connecting-to-elasticsearch\"\u003e\u003c/a\u003e\n### Connecting to ElasticSearch\n\nYou can connect with a simple connection string:\n\n```python\nfrom esorm import connect\n\n\nasync def es_init():\n    await connect('localhost:9200')\n```\n\nAlso you can connect to multiple hosts if you have a cluster:\n\n```python\nfrom esorm import connect\n\n\nasync def es_init():\n    await connect(['localhost:9200', 'localhost:9201'])\n```\n\nYou can wait for node or cluster to be ready (recommended):\n\n```python\nfrom esorm import connect\n\n\nasync def es_init():\n    await connect('localhost:9200', wait=True)\n```\n\nThis will ping the node in 2 seconds intervals until it is ready. It can be a long time.\n\nYou can pass any arguments that `AsyncElasticsearch` supports:\n\n```python\nfrom esorm import connect\n\n\nasync def es_init():\n    await connect('localhost:9200', wait=True, sniff_on_start=True, sniff_on_connection_fail=True)\n```\n\n\u003ca id=\"client\"\u003e\u003c/a\u003e\n#### Client\n\nThe `connect` function is a wrapper for the `AsyncElasticsearch` constructor. It creates and stores\na global instance of a proxy to an `AsyncElasticsearch` instance. The model operations will use this\ninstance to communicate with ElasticSearch. You can retrieve the proxy client instance and you can\nuse the same way as `AsyncElasticsearch` instance:\n\n```python\nfrom esorm import es\n\n\nasync def es_init():\n    await es.ping()\n```\n\n\u003ca id=\"create-index-templates\"\u003e\u003c/a\u003e\n### Create index templates\n\nYou can create index templates easily:\n\n```python\nfrom esorm import model as esorm_model\n\n\n# Create index template\nasync def prepare_es():\n    await esorm_model.create_index_template('default_template',\n                                            prefix_name='esorm_',\n                                            shards=3,\n                                            auto_expand_replicas='1-5')\n```\n\nHere this will be applied all `esorm_` prefixed (default) indices.\n\nAll indices created by ESORM have a prefix, which you can modify globally if you want:\n\n```python\nfrom esorm.model import set_default_index_prefix\n\nset_default_index_prefix('custom_prefix_')\n```\n\nThe default prefix is `esorm_`.\n\n\u003ca id=\"create-indices-and-mappings\"\u003e\u003c/a\u003e\n### Create indices and mappings\n\nYou can create indices and mappings automatically from your models:\n\n```python\nfrom esorm import setup_mappings\n\n\n# Create indices and mappings\nasync def prepare_es():\n    import models  # Import your models\n    # Here models argument is not needed, but you can pass it to prevent unused import warning\n    await setup_mappings(models)  \n```\n\nFirst you must create (import) all model classes. Model classes will be registered into a global registry.\nThen you can call `setup_mappings` function to create indices and mappings for all registered models.\n\n**IMPORTANT:** This method will ignore mapping errors if you already have an index with the same name. It can update the\nindices\nby new fields, but cannot modify or delete fields! For that you need to reindex your ES database. It is an ElasticSearch\nlimitation.\n\n\u003ca id=\"model-instances\"\u003e\u003c/a\u003e\n### Model instances\n\nWhen you get a model instance from elasticsearch by `search` or `get` methods, you will get the following private\nattributes filled automatically:\n\n| Attribute       | Description                         |\n|-----------------|-------------------------------------|\n| `_id`           | The ES id of the document           |\n| `_routing`      | The routing value of the document   |\n| `_version`      | Version of the document             |\n| `_primary_term` | The primary term of the document    |\n| `_seq_no`       | The sequence number of the document |\n\n\u003ca id=\"crud-create\"\u003e\u003c/a\u003e\n### CRUD: Create\n\n```python\nfrom esorm import ESModel\n\n\n# Here the model have automatically generated id\nclass User(ESModel):\n    name: str\n    age: int\n\n\nasync def create_user():\n    # Create a new user \n    user = User(name='John Doe', age=25)\n    # Save the user to ElasticSearch\n    new_user_id = await user.save()\n    print(new_user_id)\n```\n\n\u003ca id=\"crud-read\"\u003e\u003c/a\u003e\n### CRUD: Read\n\n```python\nfrom esorm import ESModel\n\n\n# Here the model have automatically generated id\nclass User(ESModel):\n    name: str\n    age: int\n\n\nasync def get_user(user_id: str):\n    user = await User.get(user_id)\n    print(user.name)\n```\n\n\u003ca id=\"crud-update\"\u003e\u003c/a\u003e\n### CRUD: Update\n\nOn update race conditions are checked automatically (with the help of _primary_term and _seq_no fields).\nThis way an optimistic locking mechanism is implemented.\n\n```python\nfrom esorm import ESModel\n\n\n# Here the model have automatically generated id\nclass User(ESModel):\n    name: str\n    age: int\n\n\nasync def update_user(user_id: str):\n    user = await User.get(user_id)\n    user.name = 'Jane Doe'\n    await user.save()\n```\n\n\u003ca id=\"crud-delete\"\u003e\u003c/a\u003e\n### CRUD: Delete\n\n```python\nfrom esorm import ESModel\n\n\n# Here the model have automatically generated id\nclass User(ESModel):\n    name: str\n    age: int\n\n\nasync def delete_user(user_id: str):\n    user = await User.get(user_id)\n    await user.delete()\n```\n\n\u003ca id=\"bulk-operations\"\u003e\u003c/a\u003e\n### Bulk operations\n\nBulk operations could be much faster than single operations, if you have lot of documents to \ncreate, update or delete.\n \nYou can use context for bulk operations:\n\n```python\nfrom typing import List\nfrom esorm import ESModel, ESBulk\n\n\n# Here the model have automatically generated id\nclass User(ESModel):\n    name: str\n    age: int\n\n\nasync def bulk_create_users():\n    async with ESBulk() as bulk:\n        # Creating or modifiying models\n        for i in range(10):\n            user = User(name=f'User {i}', age=i)\n            await bulk.save(user)\n\n\nasync def bulk_delete_users(users: List[User]):\n    async with ESBulk(wait_for=True) as bulk:  # Here we wait for the bulk operation to finish\n        # Deleting models\n        for user in users:\n            await bulk.delete(user)\n```\n\nThe `wait_for` argument is optional. If it is `True`, the context will wait for the bulk operation to finish.\n\n\u003ca id=\"search\"\u003e\u003c/a\u003e\n### Search\n\n\u003ca id=\"general-search\"\u003e\u003c/a\u003e\n#### General search\n\nYou can search for documents using `search` method, where an ES query can be specified as a dictionary.\nYou can use `res_dict=True` argument to get the result as a dictionary instead of a list. The key will be the\n`id` of the document: `await User.search(query, res_dict=True)`.\n\nIf you only need one result, you can use `search_one` method.\n\n```python\nfrom esorm import ESModel\n\n\n# Here the model have automatically generated id\nclass User(ESModel):\n    name: str\n    age: int\n\n\nasync def search_users():\n    # Search for users at least 18 years old\n    users = await User.search(\n        query={\n            'bool': {\n                'must': [{\n                    'range': {\n                        'age': {\n                            'gte': 18\n                        }\n                    }\n                }]\n            }\n        }\n    )\n    for user in users:\n        print(user.name)\n\n\nasync def search_one_user():\n    # Search a user named John Doe\n    user = await User.search_one(\n        query={\n            'bool': {\n                'must': [{\n                    'match': {\n                        'name': {\n                            'query': 'John Doe'\n                        }\n                    }\n                }]\n            }\n        }\n    )\n    print(user.name)\n```\n\nQueries are type checked, because they are annotated as `TypedDict`s. You can use IDE autocompletion and type checking.\n\n\u003ca id=\"search-with-field-value-terms-dictionary-search\"\u003e\u003c/a\u003e\n#### Search with field value terms (dictionary search)\n\nYou can search for documents using `search_by_fields` method, where you can specify a field and a value.\nIt also has a `res_dict` argument and `search_one_by_fields` variant.\n\n```python\nfrom esorm import ESModel\n\n\n# Here the model have automatically generated id\nclass User(ESModel):\n    name: str\n    age: int\n\n\nasync def search_users():\n    # Search users age is 18\n    users = await User.search_by_fields({'age': 18})\n    for user in users:\n        print(user.name)\n```\n\n\u003ca id=\"aggregations\"\u003e\u003c/a\u003e\n### Aggregations\n\nYou can use `aggregate` method to get aggregations. \nYou can specify an ES aggregation query as a dictionary. It also accepts normal ES queries,\nto be able to fiter which documents you want to aggregate. \nBoth the aggs parameter and the query parameter are type checked, because they are annotated as `TypedDict`s.\nYou can use IDE autocompletion and type checking.\n\n```python\nfrom esorm import ESModel\n\n# Here the model have automatically generated id\nclass User(ESModel):\n    name: str\n    age: int\n    country: str\n    \nasync def aggregate_avg():\n    # Get average age of users\n    aggs_def = {\n        'avg_age': {\n            'avg': {\n                'field': 'age'\n            }\n        }\n    }\n    aggs = await User.aggregate(aggs_def)\n    print(aggs['avg_age']['value'])\n    \nasync def aggregate_avg_by_country(country = 'Hungary'):\n    # Get average age of users by country\n    aggs_def = {\n        'avg_age': {\n            'avg': {\n                'field': 'age'\n            }\n        }\n    }\n    query = {\n        'bool': {\n            'must': [{\n                'match': {\n                    'country': {\n                        'query': country\n                    }\n                }\n            }]\n        }\n    }\n    aggs = await User.aggregate(aggs_def, query)\n    print(aggs['avg_age']['value'])\n    \n    \nasync def aggregate_terms():\n    # Get number of users by country\n    aggs_def = {\n        'countries': {\n            'terms': {\n                'field': 'country'\n            }\n        }\n    }\n    aggs = await User.aggregate(aggs_def)\n    for bucket in aggs['countries']['buckets']:\n        print(bucket['key'], bucket['doc_count'])\n```\n\n\u003ca id=\"pagination-and-sorting\"\u003e\u003c/a\u003e\n### Pagination and sorting\n\nYou can use `Pagination` and `Sort` classes to decorate your models. They simply wrap your models\nand add pagination and sorting functionality to them.\n\n#### Pagination\n\nYou can add a callback parameter to the `Pagination` class which will be invoked after the search with\nthe total number of documents found.\n\n```python\nfrom esorm.model import ESModel, Pagination\n\n\nclass User(ESModel):\n    id: int  # This will be used as the document _id in the index\n    name: str\n    age: int\n\n\ndef get_users(page = 1, page_size = 10):\n\n    def pagination_callback(total: int):\n        # You may set a header value or something else here\n        print(f'Total users: {total}')\n\n    # 1st create the decorator itself\n    pagination = Pagination(page=page, page_size=page_size)\n    \n    # Then decorate your model\n    res = pagination(User).search_by_fields(age=18)\n    \n    # Here the result has maximum 10 items\n    return res\n```\n\n#### Sorting\n\nIt is similar to pagination:\n\n```python\nfrom esorm.model import ESModel, Sort\n\n\nclass User(ESModel):\n    id: int  # This will be used as the document _id in the index\n    name: str\n    age: int\n    \n    \ndef get_users():\n    # 1st create the decorator itself\n    sort = Sort(sort=[\n        {'age': {'order': 'desc'}},\n        {'name': {'order': 'asc'}}\n    ])\n    \n    # Then decorate your model\n    res = sort(User).search_by_fields(age=18)\n    \n    # Here the result is sorted by age ascending\n    return res\n    \ndef get_user_sorted_by_name():\n    # You can also use this simplified syntax \n    sort = Sort(sort='name')\n    \n    # Then decorate your model\n    res = sort(User).all()\n    \n    # Here the result is sorted by age descending\n    return res\n```\n\n\u003ca id=\"testing\"\u003e\u003c/a\u003e\n## 🧪 Testing\n\nFor testing you can use the `test.sh` in the root directory. It is a script to running\ntests on multiple python interpreters in virtual environments. At the top of the file you can specify\nwhich python interpreters you want to test. The ES versions are specified in `tests/docker-compose.yml` file.\n\nIf you already have a virtual environment, simply use `pytest` to run the tests.\n\n\u003ca id=\"license\"\u003e\u003c/a\u003e\n## 🛡 License\n\nThis project is licensed under the terms of the [Mozilla Public License 2.0](https://www.mozilla.org/en-US/MPL/2.0/) (\nMPL 2.0) license.\n\n\u003ca id=\"citation\"\u003e\u003c/a\u003e\n## 📃 Citation\n\nIf you use this project in your research, please cite it using the following BibTeX entry:\n\n```bibtex\n@misc{esorm,\n  author = {Adam Wallner},\n  title = {ESORM: ElasticSearch Object Relational Mapper},\n  year = {2023},\n  publisher = {GitHub},\n  journal = {GitHub repository},\n  howpublished = {\\url{https://github.com/wallneradam/esorm}},\n} \n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwallneradam%2Fesorm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwallneradam%2Fesorm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwallneradam%2Fesorm/lists"}