Skip to content

Commit

Permalink
Support search indexes in mappings and SchemaManager (#2630)
Browse files Browse the repository at this point in the history
* Model search index mappings

* Store default search index name in ClassMetadata

* Support search indexes in SchemaManager

* Support search indexes in CLI commands

Adds default implementations for "process" methods in AbstractCommand, which throw BadMethodCallException.

Renames internal methods in ShardCommand to no longer override "index" base methods, since sharding methods in SchemaManager do much more than process indexes.

* Update phpstan baseline for unmodeled search index structs

The fields struct is recursive, which is not supported by phpstan. The analyzers struct may be technically possible to model, but the complexity isn't worth the effort.

* Require driver 1.17+ for search index APIs

* Add skip-search-indexes option to schema CLI commands

Currently, commands can either process all definitions (default behavior) or specify individual definitions. This allows the commands to rely on default behavior (e.g. $createOrder) but omit processing of search indexes, which may be more stringent requirements.

Note: this is similar to the disable-validators option that already existed in UpdateCommand; however, #2634 suggests renaming that if additional "skip" options are introduced.

* Update baseline for Psalm 5.24.0

* SearchIndex annotation docs

* Search indexes chapter
  • Loading branch information
jmikola authored May 22, 2024
1 parent 29c65ce commit fed919f
Show file tree
Hide file tree
Showing 23 changed files with 1,148 additions and 97 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
- dependencies: "lowest"
php-version: "8.1"
mongodb-version: "5.0"
driver-version: "1.11.0"
driver-version: "1.17.0"
topology: "server"
symfony-version: "stable"
# Test with highest dependencies
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@
],
"require": {
"php": "^8.1",
"ext-mongodb": "^1.11",
"ext-mongodb": "^1.17",
"doctrine/cache": "^1.11 || ^2.0",
"doctrine/collections": "^1.5 || ^2.0",
"doctrine/event-manager": "^1.0 || ^2.0",
"doctrine/instantiator": "^1.1 || ^2",
"doctrine/persistence": "^3.2",
"friendsofphp/proxy-manager-lts": "^1.0",
"jean85/pretty-package-versions": "^1.3.0 || ^2.0.1",
"mongodb/mongodb": "^1.10.0",
"mongodb/mongodb": "^1.17.0",
"psr/cache": "^1.0 || ^2.0 || ^3.0",
"symfony/console": "^5.4 || ^6.0 || ^7.0",
"symfony/deprecation-contracts": "^2.2 || ^3.0",
Expand Down
54 changes: 54 additions & 0 deletions docs/en/reference/annotations-reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1073,6 +1073,60 @@ Optional attributes:
*/
private $cart;
@SearchIndex
------------

This annotation is used to specify :ref:`search indexes <search_indexes>` for
`MongoDB Atlas Search <https://www.mongodb.com/docs/atlas/atlas-search/>`__.

The attributes correspond to arguments for
`MongoDB\Collection::createSearchIndex() <https://www.mongodb.com/docs/php-library/current/reference/method/MongoDBCollection-createSearchIndex/>`__.
Excluding ``name``, attributes are used to create the
`search index definition <https://www.mongodb.com/docs/manual/reference/command/createSearchIndexes/#search-index-definition-syntax>`__.

Optional attributes:

-
``name`` - Name of the search index to create, which must be unique to the
collection. Defaults to ``"default"``.
-
``dynamic`` - Enables or disables dynamic field mapping for this index.
If ``true``, the index will include all fields with
`supported data types <https://www.mongodb.com/docs/atlas/atlas-search/define-field-mappings/#std-label-bson-data-chart>`__.
If ``false``, the ``fields`` attribute must be specified. Defaults to ``false``.
-
``fields`` - Associative array of `field mappings <https://www.mongodb.com/docs/atlas/atlas-search/define-field-mappings/>`__
that specify the fields to index (keys). Required only if dynamic mapping is disabled.
-
``analyzer`` - Specifies the `analyzer <https://www.mongodb.com/docs/atlas/atlas-search/analyzers/>`__
to apply to string fields when indexing. Defaults to the
`standard analyzer <https://www.mongodb.com/docs/atlas/atlas-search/analyzers/standard/>`__.
-
``searchAnalyzer`` - Specifies the `analyzer <https://www.mongodb.com/docs/atlas/atlas-search/analyzers/>`__
to apply to query text before the text is searched. Defaults to the
``analyzer`` attribute, or the `standard analyzer <https://www.mongodb.com/docs/atlas/atlas-search/analyzers/standard/>`__.
if both are unspecified.
-
``analyzers`` - Array of `custom analyzers <https://www.mongodb.com/docs/atlas/atlas-search/analyzers/custom/>`__
to use in this index.
-
``storedSource`` - Specifies document fields to store for queries performed
using the `returnedStoredSource <https://www.mongodb.com/docs/atlas/atlas-search/return-stored-source/>`__
option. Specify ``true`` to store all fields, ``false`` to store no fields,
or a `document <https://www.mongodb.com/docs/atlas/atlas-search/stored-source-definition/#std-label-fts-stored-source-document>`__
to specify individual fields to include or exclude from storage. Defaults to ``false``.
-
``synonyms`` - Array of `synonym mapping definitions <https://www.mongodb.com/docs/atlas/atlas-search/synonyms/>`__
to use in this index.

.. note::

Search indexes have some notable differences from `@Index`_. They may only
be defined on document classes. Definitions will not be incorporated from
embedded documents. Additionally, ODM will **NOT** translate field names in
search index definitions. Database field names must be used instead of
mapped field names (i.e. PHP property names).

@ShardKey
---------

Expand Down
188 changes: 188 additions & 0 deletions docs/en/reference/search-indexes.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
.. _search_indexes:

Search Indexes
==============

In addition to standard :ref:`indexes <indexes>`, ODM allows you to define
search indexes for use with `MongoDB Atlas Search <https://www.mongodb.com/docs/atlas/atlas-search/>`__.
Search indexes may be queried using the `$search <https://www.mongodb.com/docs/atlas/atlas-search/aggregation-stages/search/>`__
and `$searchMeta <https://www.mongodb.com/docs/atlas/atlas-search/aggregation-stages/searchMeta/>`__
aggregation pipeline stages.

Search indexes have some notable differences from regular
:ref:`indexes <indexes>` in ODM. They may only be defined on document classes.
Definitions will not be incorporated from embedded documents. Additionally, ODM
will **NOT** translate field names in search index definitions. Database field
names must be used instead of mapped field names (i.e. PHP property names).

Search Index Options
--------------------

Search indexes are defined using a more complex syntax than regular
:ref:`indexes <indexes>`.

ODM supports the following search index options:

-
``name`` - Name of the search index to create, which must be unique to the
collection. Defaults to ``"default"``.
-
``dynamic`` - Enables or disables dynamic field mapping for this index.
If ``true``, the index will include all fields with
`supported data types <https://www.mongodb.com/docs/atlas/atlas-search/define-field-mappings/#std-label-bson-data-chart>`__.
If ``false``, the ``fields`` attribute must be specified. Defaults to ``false``.
-
``fields`` - Associative array of `field mappings <https://www.mongodb.com/docs/atlas/atlas-search/define-field-mappings/>`__
that specify the fields to index (keys). Required only if dynamic mapping is disabled.
-
``analyzer`` - Specifies the `analyzer <https://www.mongodb.com/docs/atlas/atlas-search/analyzers/>`__
to apply to string fields when indexing. Defaults to the
`standard analyzer <https://www.mongodb.com/docs/atlas/atlas-search/analyzers/standard/>`__.
-
``searchAnalyzer`` - Specifies the `analyzer <https://www.mongodb.com/docs/atlas/atlas-search/analyzers/>`__
to apply to query text before the text is searched. Defaults to the
``analyzer`` attribute, or the `standard analyzer <https://www.mongodb.com/docs/atlas/atlas-search/analyzers/standard/>`__.
if both are unspecified.
-
``analyzers`` - Array of `custom analyzers <https://www.mongodb.com/docs/atlas/atlas-search/analyzers/custom/>`__
to use in this index.
-
``storedSource`` - Specifies document fields to store for queries performed
using the `returnedStoredSource <https://www.mongodb.com/docs/atlas/atlas-search/return-stored-source/>`__
option. Specify ``true`` to store all fields, ``false`` to store no fields,
or a `document <https://www.mongodb.com/docs/atlas/atlas-search/stored-source-definition/#std-label-fts-stored-source-document>`__
to specify individual fields to include or exclude from storage. Defaults to ``false``.
-
``synonyms`` - Array of `synonym mapping definitions <https://www.mongodb.com/docs/atlas/atlas-search/synonyms/>`__
to use in this index.

Additional documentation for defining search indexes may be found in
`search index definition <https://www.mongodb.com/docs/manual/reference/command/createSearchIndexes/#search-index-definition-syntax>`__
within the MongoDB manual.

Static Mapping
--------------

`Static mapping <https://www.mongodb.com/docs/atlas/atlas-search/define-field-mappings/#static-mappings>`__
can be used to configure indexing of specific fields within a document.

The following example demonstrates how to define a search index using static
mapping.

.. configuration-block::

.. code-block:: php
<?php
/**
* @Document
* @SearchIndex(
* name="usernameAndAddresses",
* fields={
* "username"={
* {"type"="string"},
* {"type"="autocomplete"},
* },
* "addresses"={"type"="embeddedDocuments", "dynamic"=true},
* },
* )
*/
class User
{
/** @Id */
private $id;
/** @Field(type="string") */
private $username;
/** @EmbedMany(targetDocument=Address::class) */
private $addresses;
// ...
}
.. code-block:: xml
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mongo-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mongo-mapping
http://doctrine-project.org/schemas/orm/doctrine-mongo-mapping.xsd">
<document name="Documents\User">
<search-indexes>
<search-index name="usernameAndAddresses">
<field name="username" type="string" />
<field name="username" type="autocomplete" />
<field name="addresses" type="embeddedDocuments" dynamic="true" />
</search-index>
</search-indexes>
<!-- ... -->
</document>
</doctrine-mongo-mapping>
The ``username`` field will indexed both as a string and for autocompletion.
Since the ``addresses`` field uses an :ref:`embed-many <embed_many>`
relationship, it must be indexed using the ``embeddedDocuments`` type; however,
embedded documents within the array are permitted to use dynamic mapping.

Dynamic Mapping
---------------

`Dynamic mapping <https://www.mongodb.com/docs/atlas/atlas-search/define-field-mappings/#dynamic-mappings>`__
can be used to automatically index fields with
`supported data types <https://www.mongodb.com/docs/atlas/atlas-search/define-field-mappings/#std-label-bson-data-chart>`__
within a document. Dynamically mapped indexes occupy more disk space than
statically mapped indexes and may be less performant; however, they may be
useful if your schema changes or for when experimenting with Atlas Search

.. note::

Atlas Search does **NOT** dynamically index embedded documents contained
within arrays (e.g. :ref:`embed-many <embed_many>` relationships). You must
use static mappings with the `embeddedDocument <https://www.mongodb.com/docs/atlas/atlas-search/field-types/embedded-documents-type/>`__
field type.

The following example demonstrates how to define a search index using dynamic
mapping:

.. configuration-block::

.. code-block:: php
<?php
/**
* @Document
* @SearchIndex(dynamic=true)
*/
class BlogPost
{
/** @Id */
private $id;
/** @Field(type="string") */
private $title;
/** @Field(type="string") */
private $body;
// ...
}
.. code-block:: xml
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mongo-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mongo-mapping
http://doctrine-project.org/schemas/orm/doctrine-mongo-mapping.xsd">
<document name="Documents\BlogPost">
<search-indexes>
<search-index dynamic="true" />
</search-indexes>
<!-- ... -->
</document>
</doctrine-mongo-mapping>
1 change: 1 addition & 0 deletions docs/en/sidebar.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
reference/bidirectional-references
reference/complex-references
reference/indexes
reference/search-indexes
reference/inheritance-mapping
reference/embedded-mapping
reference/trees
Expand Down
91 changes: 91 additions & 0 deletions doctrine-mongo-mapping.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
<xs:element name="lifecycle-callbacks" type="odm:lifecycle-callbacks" minOccurs="0" />
<xs:element name="also-load-methods" type="odm:also-load-methods" minOccurs="0" />
<xs:element name="indexes" type="odm:indexes" minOccurs="0" />
<xs:element name="search-indexes" type="odm:search-indexes" minOccurs="0" />
<xs:element name="shard-key" type="odm:shard-key" minOccurs="0" />
<xs:element name="read-preference" type="odm:read-preference" minOccurs="0" />
<xs:element name="schema-validation" type="odm:schema-validation" minOccurs="0" />
Expand Down Expand Up @@ -466,6 +467,96 @@
</xs:choice>
</xs:complexType>

<xs:complexType name="search-indexes">
<xs:choice maxOccurs="unbounded">
<xs:element name="search-index" type="odm:search-index" maxOccurs="unbounded" />
</xs:choice>
</xs:complexType>

<!-- https://www.mongodb.com/docs/manual/reference/command/createSearchIndexes/ -->
<xs:complexType name="search-index">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="field" type="odm:search-index-field" minOccurs="0" maxOccurs="unbounded" />
<!-- Note: custom analyzers are intentionally unsupported in XML -->
<xs:element name="synonym" type="odm:search-index-synonym" minOccurs="0" maxOccurs="unbounded" />
<xs:element name="stored-source" type="odm:search-index-stored-source" minOccurs="0" maxOccurs="1" />
</xs:choice>

<xs:attribute name="name" type="xs:string" />
<xs:attribute name="dynamic" type="xs:boolean" />
<xs:attribute name="analyzer" type="xs:string" />
<xs:attribute name="searchAnalyzer" type="xs:string" />
<xs:attribute name="storedSource" type="xs:boolean" />
</xs:complexType>

<!-- https://www.mongodb.com/docs/atlas/atlas-search/define-field-mappings/ -->
<xs:complexType name="search-index-field">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<!-- https://www.mongodb.com/docs/atlas/atlas-search/field-types/document-type/ -->
<!-- https://www.mongodb.com/docs/atlas/atlas-search/field-types/embedded-documents-type/ -->
<xs:element name="field" type="odm:search-index-field" minOccurs="0" maxOccurs="unbounded" />
<!-- https://www.mongodb.com/docs/atlas/atlas-search/field-types/string-type/ -->
<xs:element name="multi" type="odm:search-index-field" minOccurs="0" maxOccurs="unbounded" />
</xs:choice>

<xs:attribute name="name" type="xs:string" use="required" />
<xs:attribute name="type" type="xs:string" use="required" />
<!-- https://www.mongodb.com/docs/atlas/atlas-search/field-types/autocomplete-type/ -->
<xs:attribute name="maxGrams" type="xs:integer" />
<xs:attribute name="minGrams" type="xs:integer" />
<xs:attribute name="tokenization" type="xs:string" />
<xs:attribute name="foldDiacritics" type="xs:boolean" />
<!-- https://www.mongodb.com/docs/atlas/atlas-search/field-types/document-type/ -->
<!-- https://www.mongodb.com/docs/atlas/atlas-search/field-types/embedded-documents-type/ -->
<xs:attribute name="dynamic" type="xs:boolean" />
<!-- https://www.mongodb.com/docs/atlas/atlas-search/field-types/geo-type/ -->
<xs:attribute name="indexShapes" type="xs:boolean" />
<!-- https://www.mongodb.com/docs/atlas/atlas-search/field-types/knn-vector/ -->
<xs:attribute name="dimensions" type="xs:integer" />
<xs:attribute name="similarity" type="xs:string" />
<!-- https://www.mongodb.com/docs/atlas/atlas-search/field-types/number-type/ -->
<!-- https://www.mongodb.com/docs/atlas/atlas-search/field-types/number-facet-type/ -->
<xs:attribute name="representation" type="xs:string" />
<xs:attribute name="indexIntegers" type="xs:boolean" />
<xs:attribute name="indexDoubles" type="xs:boolean" />
<!-- https://www.mongodb.com/docs/atlas/atlas-search/field-types/string-type/ -->
<xs:attribute name="analyzer" type="xs:string" />
<xs:attribute name="searchAnalyzer" type="xs:string" />
<xs:attribute name="indexOptions" type="xs:string" />
<xs:attribute name="store" type="xs:boolean" />
<xs:attribute name="ignoreAbove" type="xs:integer" />
<xs:attribute name="norms" type="xs:string" />
<!-- https://www.mongodb.com/docs/atlas/atlas-search/field-types/token-type/ -->
<xs:attribute name="normalizer" type="xs:string" />
</xs:complexType>

<!-- https://www.mongodb.com/docs/atlas/atlas-search/synonyms/ -->
<xs:complexType name="search-index-synonym">
<xs:attribute name="name" type="xs:string" use="required" />
<xs:attribute name="analyzer" type="xs:string" use="required" />
<xs:attribute name="sourceCollection" type="xs:string" use="required" />
</xs:complexType>

<!-- https://www.mongodb.com/docs/atlas/atlas-search/stored-source-definition -->
<xs:complexType name="search-index-stored-source">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="field" type="odm:search-index-stored-source-field" minOccurs="0" maxOccurs="unbounded" />
</xs:choice>

<xs:attribute name="type" type="odm:search-index-stored-source-type" use="required" />
</xs:complexType>

<xs:complexType name="search-index-stored-source-field">
<xs:attribute name="name" type="xs:string" use="required" />
</xs:complexType>

<xs:simpleType name="search-index-stored-source-type">
<xs:restriction base="xs:token">
<xs:enumeration value="include" />
<xs:enumeration value="exclude" />
</xs:restriction>
</xs:simpleType>

<xs:complexType name="shard-key">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="key" type="odm:shard-key-key" maxOccurs="unbounded" />
Expand Down
Loading

0 comments on commit fed919f

Please sign in to comment.