In this example, event notifications from the ContentManagementApplication
from /topics/examples/content-management
are processed and projected into an eventually-consistent full text search index, a searchable "materialized view" of the pages' body text just like /topics/examples/searchable-content
.
This is an example of CQRS. By separating the search engine "read model" from the content management "write model", the commands that update pages will perform faster. But, more importantly, the search engine can be redesigned and rebuilt by reprocessing those events. The projected searchable content can be deleted and rebuilt, perhaps also to include page titles, or timestamps, or other information contained in the domain events such as the authors, because it is updated by processing events. This is the main advantage of "CQRS" over the "inline" technique used in /topics/examples/searchable-content
where the search index is simply updated whenever new events are recorded. Please note, it is possible to migrate from the "inline" technique to CQRS, by adding the downstream processing and then removing the inline updating, since the domain model is already event sourced. Similarly, other projections can be added to work alongside and concurrently with the updating of the search engine.
The SearchIndexApplication
defined below is a ~eventsourcing.system.ProcessApplication
. Its policy()
function is coded to process the Page.Created
and Page.BodyUpdated
domain events of the ContentManagementApplication
. It also has a search()
method that returns a list of page IDs.
The SearchIndexApplication
class in this example works in a similar way to the SearchableContentApplication
class in /topics/examples/searchable-content
, by setting variable keyword arguments insert_pages
and update_pages
on a the ~eventsourcing.application.ProcessingEvent
object. However, rather than populating the variable keyword arguments in the save()
method, it populates insert_pages
and update_pages
within its policy()
function. The insert_pages
and update_pages
arguments are set on the ~eventsourcing.application.ProcessingEvent
object passed into the policy()
function, which carries an event notification ID that indicates the position in the application sequence of the domain event that is being processed.
The application will be configured to run with a custom ~eventsourcing.persistence.ProcessRecorder
so that search index records will be updated atomically with the inserting of a tracking record which indicates which upstream event notification has been processed.
Because the Page.BodyUpdated
event carries only the diff
of the page body, the policy()
function must first select the current page body from its own records and then apply the diff as a patch. The "exactly once" semantics provided by the library's system module guarantees that the diffs will always be applied in the correct order. Without this guarantee, the projection could become inconsistent, with the consequence that the diffs will fail to be applied.
../../../eventsourcing/examples/contentmanagementsystem/application.py
A ~eventsourcing.system.System
of applications is defined, in which the SearchIndexApplication
follows the ContentManagementApplication
. This system can then be used in any ~eventsourcing.system.Runner
.
../../../eventsourcing/examples/contentmanagementsystem/system.py
The PostgresSearchableContentRecorder
from /topics/examples/searchable-content
is used to define a custom ~eventsourcing.persistence.ProcessRecorder
for PostgreSQL. The PostgreSQL ~eventsourcing.postgres.Factory
class is extended to involve this custom recorder in a custom persistence module so that it can be used by the SearchIndexApplication
.
../../../eventsourcing/examples/contentmanagementsystem/postgres.py
The SqliteSearchableContentRecorder
from /topics/examples/searchable-content
is used to define a custom ~eventsourcing.persistence.ProcessRecorder
for SQLite. The SQLite ~eventsourcing.sqlite.Factory
class is extended to involve this custom recorder in a custom persistence module so that it can be used by the SearchIndexApplication
.
../../../eventsourcing/examples/contentmanagementsystem/sqlite.py
The test case ContentManagementSystemTestCase
creates three pages, for 'animals', 'plants' and 'minerals'. Content is added to the pages. The content is searched with various queries and the search results are checked. The test is executed twice, once with the application configured for both PostgreSQL, and once for SQLite.
../../../eventsourcing/examples/contentmanagementsystem/test_system.py