Alfresco Community 26.2, released in July 2026, drops support for Apache Solr and introduces Alfresco Search Community, a search module that indexes the repository into a standard OpenSearch or Elasticsearch cluster. This post covers it at the level of detail you need in order to actually deploy it: every configuration property, where the artifacts come from, which Docker images to pin, how to migrate an existing repository, and what happens to the queries you already have.
Status: generally available. Alfresco Search Community ships with the 26.2 GA release and is intended for production use. It is distributed as binaries only, a Docker image and a JAR, and the final license text has not been published yet. Both points are covered at the end.
A note on names. The product is called Alfresco Search Community, but every technical identifier keeps the legacy vocabulary: the repository subsystem is
elasticsearch, the shared-secret property issolr.sharedSecret, the text-extraction endpoint is/alfresco/service/api/solr/textContent, and the artifact isalfresco-elasticsearch-batch-indexing. That is deliberate and stable. It also means the word "elasticsearch" in a property name does not imply Elasticsearch the product: OpenSearch is configured through exactly the same properties.
Two things, and the second exists because of the first.
elasticsearch repository subsystem that Alfresco Search Enterprise uses, and it is distributed as binaries only. Source code is not published today; the stated intention is 27.1 or later.Functionally it is the equivalent of Alfresco Search Enterprise, but the indexing mechanism is different: instead of consuming repository events, it polls the repository database for changed transactions. That distinction drives most of the operational detail in this post, and it gets its own section further down.
All three of the usual deployment routes already support it
It helps to see where this sits historically, because the 2026 change is smaller than it looks in one respect and much bigger in another.
Search Engines in Alfresco Community
Polling-based tracking is not new. Alfresco has pulled changes on a timer since 2011, and Alfresco Search Community keeps doing exactly that, just with a 30 second default instead of 15 and against a different target. However, for the first time the index lives in an engine that Alfresco does not ship, patch, or wrap. There is no Alfresco plugin on the search cluster. You run stock OpenSearch or stock Elasticsearch, and you can run it as a managed service.
There is a community answer, and it is worth knowing about even if you are moving to OpenSearch. jecicorp/AlfrescoSearchServices is a fork of Alfresco Search Services maintained by Jeci, modernising the project onto Apache Solr 9 and Java 17.
You can stand it up with alfresco-docker-installer if you want to compare the two engines side by side. There is a longer write-up of that project on this blog: Bring Alfresco Search to 2026: Vanilla Solr 9, Java 17, and an invitation to the Community.
Four products, two repository subsystems. The subsystem is what matters technically, because it determines which query languages and which fields are available.
solr6. Community Edition, from before 7.1 up to and including 23.2. Not an option on 26.2.solr6. Enterprise only, from before 7.1 up to 7.1+. Gone by 23.2.elasticsearch. Community Edition, new in 26.2. This is the subject of this post.elasticsearch. Enterprise only, from 7.1 onwards, still current in 26.2.Alfresco Search Family
The practical consequence: if you are on Community 26.2 you are on the elasticsearch subsystem, which is the same subsystem Enterprise customers have been using since 7.1. Everything documented about the query behaviour of that subsystem applies to you now, including its limitations.
The official documentation splits it into two cooperating parts, and that split is the single most useful thing to internalise:
Those two halves are deployed, configured, versioned, and monitored separately. Half the configuration properties in this post belong to the repository and half belong to the indexer, and mixing them up is the most common early mistake.
No Alfresco plugin is installed on the search cluster. That is what makes a managed OpenSearch or Elasticsearch service viable.
This is the correction most worth flagging, because the 26.2-preview announcement said PostgreSQL only and that is no longer accurate. The batch-indexing application reads node metadata directly from the repository database, and the supported list is:
For anything other than PostgreSQL, drop the driver JAR into /opt/db-drivers. The Docker image declares that path as a volume and the application loads drivers from it at startup, through a Spring Boot PropertiesLauncher with Loader-Path: db-drivers/. Mount the directory read-only.
services:
alfresco-elasticsearch-batch-indexing:
image: alfresco/alfresco-elasticsearch-batch-indexing:5.7.1
volumes:
- ./db-drivers:/opt/db-drivers:ro
environment:
SPRING_DATASOURCE_URL: jdbc:mariadb://mariadb:3306/alfresco
The read-only mount is not decoration. The indexer's own threat model calls it out: the driver directory is executable code loaded at startup, so it should not be writable by the running container.
The whole system, with the numbered hops you will see in the logs and in the metrics.
High Level Architecture
Some properties of this design that are easy to miss:
/alfresco/service/api/solr/textContent over HTTP, authenticated with the X-Alfresco-Search-Secret header. It never touches alf_data.jdbc:hsqldb:mem:batch). Job history is therefore ephemeral and does not survive a restart, which is intentional but has an operational consequence covered later.alfresco. Holds the searchable documents: metadata, content, and path. This is the index Content Services queries.alfresco-reindex-state. Holds the indexing cursor, a single watermark document. Hidden.alfresco-reindex-dead-letter. Records items that could not be indexed. Hidden.Hidden is not access control. The state and dead-letter indexes are created with
index.hidden: trueso that they do not show up in index listings. That is a convenience, not a security boundary. Use search-cluster permissions to protect them.
Because they are hidden, the obvious command does not show them. You need expand_wildcards=all:
# shows only "alfresco"
GET _cat/indices?v&s=index
# shows all three
GET _cat/indices/alfresco*?v&s=index&expand_wildcards=all
# proves the hidden flag
GET alfresco*/_settings?expand_wildcards=all&filter_path=**.hidden
There is a fourth index name in play, belonging to the repository rather than to the indexer: elasticsearch.archive.indexName, defaulting to alfresco-archive, which the Configure documentation describes as the index used for deleted (archived) nodes.
Deleting a node in Alfresco does not erase it. The node moves from workspace://SpacesStore to archive://SpacesStore, the store behind the trashcan, and the repository picks an index from the store protocol of the search request. SearchRequestBuilderService.getElasticIndex() maps workspace to elasticsearch.indexName, archive to elasticsearch.archive.indexName, and throws on any other protocol. Through the v1 Search API you reach the second one by scoping a request to deleted-nodes.
That routing is the whole of the feature. Nothing writes to the index, and nothing creates it either:
elasticsearch.createIndexIfNotExists only ever covers the main index. ElasticsearchInitialiser and ContentModelSynchronizer work on indexName and never on the archive name.5.7.1 image contains no reference to alfresco-archive, to archive.indexName, or to the archive store, only to the three indexes above.So the property is a query route to an index no component fills. Left alone that is not an empty result but a failure, because nothing sets ignore_unavailable and the repository rethrows any search error that is not a highlighting error:
curl -s -u admin:admin -X POST \
http://localhost:8080/alfresco/api/-default-/public/search/versions/1/search \
-H 'Content-Type: application/json' \
-d '{"query":{"query":"*"},"scope":{"locations":["deleted-nodes"]}}'
{"error":{"statusCode":500,
"briefSummary":"Request failed: [index_not_found_exception] no such index [alfresco-archive]"}}
Do not reach for the obvious workaround. Creating alfresco-archive by hand does make the request succeed, with zero results, and that is worse than the error: an empty result set is indistinguishable from "no deleted nodes matched", so the caller cannot tell a missing feature from a genuine absence of hits.
Two related facts. Deletion is handled correctly on the main index: a node moved to the trashcan is removed from alfresco within one indexing cycle, so trashcan content does not linger in ordinary results. And Solr 6 did index deleted nodes, into a second core declared for archive://SpacesStore. Indexed access to deleted content is therefore a capability lost in the move to Alfresco Search Community, not one merely left un-configured. Check whether anything depends on it before you switch.
One non-obvious detail that will save you an afternoon. Alfresco property names are escaped in the index mapping: : becomes %3A and . becomes %2E. So cm:name is stored as the field cm%3Aname, and cm:content.mimetype is stored as cm%3Acontent%2Emimetype.
In a JSON request body you write the single-escaped form. In a URL path or query parameter you have to double-escape it, or you get an empty result with no error:
# in a body: single escape
GET alfresco/_search
{"query":{"term":{"cm%3Aname":"budget.xls"}}}
# in a URL: double escape, or you silently get nothing
GET alfresco/_search?q=cm%253Aname:budget.xls
Alongside the escaped property fields there is a set of uppercase system fields, around 32 of them in a stock index, including TYPE, ASPECT, PATH, UNPREFIXED_PATH, ANCESTOR, PARENT, PRIMARYPARENT, READER, DENIED, OWNER, ALIVE, SITE, TAG and PROPERTIES. A stock 26.2 index with the sample sites carries roughly 950 mapped fields and six custom analyzers (locale_text_index, locale_text_query, locale_content, path_emulator and two cross-locale variants).
One more subtlety on paths, which trips people up when they try to hand-write a subtree query: a match_phrase on PATH matches the node itself, not its descendants. Descendant queries use a prefix on UNPREFIXED_PATH. In practice you should let AFTS build these, but it explains why a hand-rolled PATH query returns 1 hit where you expected 69.
This is the loop that replaces Solr trackers. It runs inside ContinuousReindexingService, every pollingInterval (30 seconds by default), and the whole behaviour of the system falls out of it.
Sync Cycle
Four consequences worth stating explicitly, because each one turns into a support question:
overlap (10 minutes) further than strictly necessary, so a change committed right on a window boundary is not missed. It also means documents get rewritten harmlessly on every cycle they fall inside.now - overlap. This is the single most important default in the whole product: pre-existing content is not indexed automatically. A fresh indexer pointed at a repository with ten years of history will index the last ten minutes and consider itself up to date. Getting the history in is a manual procedure, covered in the migration section.OpenSearch does not know about Alfresco content models, and the alfresco index is created with dynamic: false, meaning unmapped fields are silently ignored rather than auto-mapped. So something has to translate model changes into mapping updates. That something lives in the repository, not in the indexer.
Content Model Sync
Two things follow. First, adding a property to a custom model does not require a reindex; the mapping is extended and new documents pick it up. Second, elasticsearch.index.mapping.total_fields.limit (default 7500) is a real ceiling, and because a stock index already uses around 950 fields, a model with a few thousand properties can hit it. When it does, the failure comes from OpenSearch as a native mapping error, not from Alfresco.
Note that this synchronizer is part of the elasticsearch subsystem in the repository. It only runs when that subsystem is active, which matters for the migration ordering discussed later.
Model synchronization takes care of the mapping. It does not take care of the indexer, and that is where the sharpest edge in this module sits for anyone running a content model of their own.
The repository stores a property's identity as a namespace URI plus a local name. The prefix, cm or sys or your own, lives in the model definition the dictionary loads at runtime, not in the database. Index field names are built from the prefixed name, so something has to turn http://www.alfresco.org/model/content/1.0 into cm. The repository does that with its own NamespaceService. The batch indexer cannot: it reads nodes over JDBC, where the mapping does not exist. It uses a static JSON file instead, alfresco.reindex.prefixes-file, which defaults to classpath:reindex.prefixes-file.json inside the JAR and lists 60 namespaces, all of them Alfresco's.
A model in a namespace that file does not list fails in one of two ways, both silent. Measured on 26.2.0 with batch indexing 5.7.1:
hr:contract, is not indexed at all. No document, and the dead-letter index stays empty: the node counts as filtered, so filterCount rises while readCount and writeCount look healthy.cm:content node that merely carries a custom aspect is indexed, but without the aspect's properties, and with the aspect itself missing from its ASPECT field.The only signal is an ERROR in the indexer log:
o.a.r.processors.AlfrescoNodeProcessor : impossible to get prefixed name of contractNumber
o.a.r.processors.AlfrescoNodeProcessor : impossible to retrieve type name for node 874
Queries then return fewer results than they should, which is the mirror image of the silently dropped query conditions discussed later in this post, and just as quiet.
The file replaces the shipped map, it does not extend it. A file holding only your namespace removes Alfresco's 60, and the indexer can then no longer resolve
sys:versionMajorwhile validating the repository schema. That failure is loud:validateDbSchemaStepfails withNumberFormatException: Cannot parse null stringand nothing is indexed at all. Passing a single entry as a JVM system property,-DprefixUriMap[uri]=prefix, breaks in exactly the same way, because the system property takes precedence over the whole map rather than adding a key to it.
So the file has to be complete, and the repository is the only component that knows every deployed model. It will hand the whole map over. model-ns-prefix-mapping is a small Alfresco Labs addon, Apache-2.0 and ACS 7.0 or later, that adds one read-only WebScript returning exactly the JSON structure the indexer consumes, for every namespace in the dictionary including yours. It is not part of the product and carries no support, but it reads the dictionary and writes nothing.
Install the release JAR in the repository webapp and restart it. The Docker image carries an exploded webapp, so a bind mount is enough:
alfresco:
volumes:
- ./model-ns-prefix-mapping-1.2.0.jar:/usr/local/tomcat/webapps/alfresco/WEB-INF/lib/model-ns-prefix-mapping-1.2.0.jar:ro
Deploy your model, then ask the repository for the map:
curl -s -u admin:admin http://localhost:8080/alfresco/s/model/ns-prefix-map > prefixes.json
The dictionary is read live, so no repository restart is needed between deploying a model and fetching a map that covers it. And what comes back is not the shipped file plus your namespace. On a stock Community 26.2 repository it holds 64 entries against the shipped file's 60, and differs in both directions: the shipped file carries four namespaces that only exist in Enterprise deployments (abs, devicesync, hwf, sync) and misses eight that Community registers, among them the IPTC and XMP metadata namespaces. Generating the map from the repository is what makes it match the repository being indexed.
Then mount the result and point the indexer at it:
batch-indexer:
environment:
JAVA_OPTS: -Dalfresco.reindex.prefixes-file=file:/config/prefixes.json
volumes:
- ./prefixes.json:/config/prefixes.json:ro
It has to be JAVA_OPTS rather than an environment variable, because the property is read through a Spring @PropertySource that resolves before the relaxed binding which would turn ALFRESCO_REINDEX_PREFIXESFILE into a property name. The indexer never talks to the addon: once the file exists, the addon can stay for the next model you deploy or be unmounted again.
Correcting the file does not revisit nodes the indexer has already passed, because it walks forward through transaction commit times. Touch a handful of nodes to bring them back into the window, or, if the model was in use all along, reseed the cursor and reindex from the start of history as described in the migration section.
Nothing outside the repository can find out that a model changed. There is no model event on the repository event stream and no marker in the database. POST /api/solr/modelsdiff looks like the answer and is not: under the elasticsearch subsystem it returns an empty diff list forever, because the tracking component behind it short-circuits when search.solrTrackingSupport.enabled is false, which that subsystem sets by default, and an empty diff list is indistinguishable from no change. Deploying a model and refreshing the prefix map is a deliberate sequence, not something that can be automated by watching the repository.
Which makes a check worth having, because the alternative signal is a user reporting missing content. The reference deployments in alfresco-search-community-deployments ship two scripts for this. tools/fetch-prefix-map.sh generates the map and refuses to write one that is missing Alfresco's own namespaces, which is the loud failure described above. tools/check-prefix-map.sh asks the repository what it has deployed, compares that against the file the indexer will read, and turns the silence into an exit status:
./tools/check-prefix-map.sh config/prefixes.json
It needs no node to exist, so it can run while the indexer is still stopped. On a migration that is the only point at which you can still act on the answer cheaply, because history is walked once in ascending commit time. It reads the repository over HTTP and writes nothing, so it is safe to point at production and usable in CI against a staging repository. The full procedure, including which nodes need touching once the map is right, is in docs/custom-content-models.md in the same repository.
The silence is a defect, not a step you were meant to discover. It is tracked as ACS-12851, which needs a Hyland account to read. A node whose namespace cannot be resolved is a data-loss condition and belongs in the dead-letter index with the URI that failed, or the chunk should fail outright; better still, the indexer would resolve prefixes from the repository, which already serves that answer to other callers, rather than from a file baked into an image that cannot be correct for an installation with custom models. Until one of those lands, the check above is the earliest signal available.
The distribution can be downloaded from Nexus Alfresco: alfresco-elasticsearch-batch-indexing-distribution-5.7.1.zip. Inside the zip you get the executable Spring Boot JAR.
The image is alfresco/alfresco-elasticsearch-batch-indexing, published on Docker Hub. The image is deliberately minimal:
FROM alfresco/alfresco-base-java:jre17-rockylinux9
WORKDIR /opt
COPY *-app.jar app.jar
RUN mkdir -p /opt/db-drivers
VOLUME /opt/db-drivers
ENTRYPOINT exec java $JAVA_OPTS -jar app.jar
JRE 17 on Rocky Linux 9, one JAR, one volume for JDBC drivers, and JAVA_OPTS as the escape hatch for JVM flags such as a custom truststore. Note that the batch indexer is published on Docker Hub under alfresco/, whereas the Enterprise live-indexing images live on quay.io/alfresco/.
alfresco/alfresco-content-repository-community:26.2.0alfresco/alfresco-elasticsearch-batch-indexing:5.7.1alfresco/alfresco-transform-core-aio:5.4.3opensearchproject/opensearch:2.11.1opensearchproject/opensearch-dashboards:2.11.1postgres:15.6Pin the OpenSearch tag. A floating opensearchproject/opensearch:2 will drift off the one supported patch release, 2.11.1.
If you just want a running 26.2 stack with OpenSearch to poke at, the generator does all of the above for you:
npm install -g yo generator-alfresco-docker-installer
yo alfresco-docker-installer \
--acsVersion=26.2 \
--searchType=opensearch \
--ram=16 \
--serverName=localhost \
--port=8080
docker compose up -d --build
That produces a full stack: repository, Share, Content App, Transform Core, PostgreSQL, OpenSearch, OpenSearch Dashboards, the batch indexer, and an nginx proxy. Two details from the generated output that are instructive in themselves. The proxy explicitly blocks the text-extraction endpoint from outside:
location ~ ^(/.*/service/api/solr/.*)$ { return 403; }
And OpenSearch itself is not published to the host at all, so the way in is OpenSearch Dashboards on port 5601 (/app/dev_tools#/console), which is a much nicer place to explore the index than curl anyway.
The documented invocation, with credentials on the command line so they are not left on the filesystem:
java -jar alfresco-elasticsearch-batch-indexing-5.7.1-app.jar \
--spring.elasticsearch.uris=http://localhost:9200 \
--spring.datasource.url=jdbc:postgresql://localhost:5432/alfresco \
--spring.datasource.username=alfresco \
--spring.datasource.password=<database-password> \
--alfresco.acs.url=http://localhost:8080 \
--alfresco.acceptedContentMediaTypesCache.baseurl=http://localhost:8090/transform/config \
--alfresco.content.transform.sharedSecret=<shared-secret>
Six arguments and you are indexing. The one that has no sane default is sharedSecret, and it has to match solr.sharedSecret in the repository or every content fetch returns 401.
A complete, working minimal stack. Replace the two placeholder secrets before using it anywhere real:
services:
postgres:
image: postgres:15.6
environment:
POSTGRES_USER: alfresco
POSTGRES_PASSWORD: <database-password>
POSTGRES_DB: alfresco
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U alfresco"]
interval: 10s
retries: 5
transform-core-aio:
image: alfresco/alfresco-transform-core-aio:5.4.3
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8090/ready"]
interval: 15s
retries: 10
start_period: 30s
opensearch:
image: opensearchproject/opensearch:2.11.1
environment:
discovery.type: single-node
DISABLE_SECURITY_PLUGIN: "true"
OPENSEARCH_JAVA_OPTS: -Xms1g -Xmx1g
volumes:
- opensearch-data:/usr/share/opensearch/data
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:9200/_cluster/health || exit 1"]
interval: 30s
retries: 5
start_period: 1m
alfresco:
image: alfresco/alfresco-content-repository-community:26.2.0
depends_on:
postgres:
condition: service_healthy
transform-core-aio:
condition: service_healthy
environment:
JAVA_OPTS: >-
-Ddb.driver=org.postgresql.Driver
-Ddb.username=alfresco
-Ddb.password=<database-password>
-Ddb.url=jdbc:postgresql://postgres:5432/alfresco
-Dindex.subsystem.name=elasticsearch
-Delasticsearch.host=opensearch
-Delasticsearch.port=9200
-Delasticsearch.indexName=alfresco
-Delasticsearch.createIndexIfNotExists=true
-Dsolr.secureComms=secret
-Dsolr.sharedSecret=<shared-secret>
-DlocalTransform.core-aio.url=http://transform-core-aio:8090/
volumes:
- alf-data:/usr/local/tomcat/alf_data
ports:
- "8080:8080"
batch-indexer:
image: alfresco/alfresco-elasticsearch-batch-indexing:5.7.1
depends_on:
alfresco:
condition: service_started
opensearch:
condition: service_healthy
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/alfresco
SPRING_DATASOURCE_USERNAME: alfresco
SPRING_DATASOURCE_PASSWORD: <database-password>
SPRING_ELASTICSEARCH_URIS: http://opensearch:9200
ELASTICSEARCH_INDEXNAME: alfresco
ALFRESCO_ACS_URL: http://alfresco:8080
ALFRESCO_ACCEPTEDCONTENTMEDIATYPESCACHE_BASEURL: http://transform-core-aio:8090/transform/config
ALFRESCO_CONTENT_TRANSFORM_SHAREDSECRET: <shared-secret>
ports:
- "8091:8080"
volumes:
db-data:
alf-data:
opensearch-data:
Then:
docker compose up -d
curl "http://localhost:9200/_cat/indices?v&expand_wildcards=all"
docker compose logs -f batch-indexer
Two things in that file that are not obvious.
solr.secureComms=secretis mandatory, not optional: the X509 filter on the text-extraction endpoint no longer acceptsnonein 26.2, so leaving it unset breaks content indexing while metadata keeps working. AndDISABLE_SECURITY_PLUGINon OpenSearch is a completely separate concern fromsolr.secureComms; the first is about the search cluster's own authentication, the second is about the repository's endpoint. Turning one on does not affect the other.
Reference Docker Compose deployments of the Alfresco Community 26.2 stack with Alfresco Search Community (OpenSearch) available in https://github.com/aborroy/alfresco-search-community-deployments
These go in alfresco-global.properties (or the Repository Admin Console). They configure the query path: how Content Services talks to the search cluster and how it builds queries. The canonical file with all of the defaults is elasticsearch.properties in alfresco-community-repo, and unlike the indexer it is open source, so you can always check a default yourself.
index.subsystem.name = elasticsearch. Selects Alfresco Search Community instead of solr6. This is the switch.solr.secureComms = secret. Required. Protects the text-extraction endpoint with a shared-secret header.solr.sharedSecret, no default. Must match alfresco.content.transform.sharedSecret on the indexer.elasticsearch.host, default localhost. Search cluster host.elasticsearch.port, default 9200. Search cluster port.elasticsearch.baseUrl, default /. Path prefix, for clusters served behind a sub-path.elasticsearch.indexName, default alfresco. Main index. Must match the indexer elasticsearch.indexName.elasticsearch.createIndexIfNotExists, default false. Create the index and its mapping at startup if absent. Note the shipped default is false, while the sample configuration in the Install documentation sets it to true. For a first deployment you want true.elasticsearch.secureComms, default none. none for plain HTTP, https for one-way TLS. With https the server certificate is verified against the repository truststore (encryption.ssl.truststore.*).elasticsearch.ssl.host.name.verification, default false. Set to false when the server certificate has no CN matching the DNS hostname.elasticsearch.auth.mode, default basic. basic or aws-iam. These are the only two values.elasticsearch.user, empty by default. HTTP Basic user name.elasticsearch.password, empty by default. HTTP Basic password.elasticsearch.aws.region, empty by default. AWS region for SigV4 signing when auth.mode=aws-iam.elasticsearch.aws.service, default es. AWS signing service name.elasticsearch.max.total.connections, default 30elasticsearch.max.host.connections, default 30elasticsearch.http.socket.timeout, default 30000 mselasticsearch.http.connection.timeout, default 1000 mselasticsearch.http.response.timeout, default 30000 mselasticsearch.retryPeriodSeconds, default 10elasticsearch.retryAttempts, default 3elasticsearch.lockRetryAttempts, default 3elasticsearch.lockRetryPeriodSeconds, default 10elasticsearch.index.locale, empty by default. Locale used to select the language analyzer for text fields. Set it to your primary content language for better tokenization and matching.elasticsearch.index.custom.analyzer.config.files, empty by default. Comma-separated file: or classpath: locations for custom analyzer definitions.elasticsearch.index.mapping.total_fields.limit, default 7500. Maximum number of fields in the index mapping.elasticsearch.index.max_result_window, default 10000. Maximum results a single query can return.elasticsearch.scroll.api_time, default 10s. Scroll context lifetime.elasticsearch.scroll.batch_size, default 100. Documents per scroll batch.elasticsearch.defaultFacetLimit, default 100. Maximum facets returned by a single query.elasticsearch.query.analyzeWildcard.fields, default cm:content,cm:persondescription,cm:preferenceValues,cm:tagScopeCache,sys:keyStore,sys:versionEdition,sys:versionProperties. Fields where wildcard terms are analyzed, which are the fields using the locale_content analyzer. See the query-compatibility section, because this list has real consequences.elasticsearch.query.includeGroupsForRoleAdmin, default false. Whether admin queries expand group membership.query.fts.queryConsistency, default TRANSACTIONAL_IF_POSSIBLE. Runs an AFTS query against the database when it is eligible, and against the search cluster otherwise.query.cmis.queryConsistency, default TRANSACTIONAL_IF_POSSIBLE. The same for CMIS.query.hybrid.enabled, default false. Hybrid database/index execution.This is Transactional Metadata Query, and it is unchanged from solr6. It matters here for a debugging reason: a metadata-only query can be answered entirely from the database and never reach OpenSearch at all. If you are testing whether the index behaves a certain way, a plain metadata query may be a false negative. The switching logic skips the database path when the request has faceting, so attaching any facet forces index execution. That is exactly the trick the compatibility test script described later relies on.
system.acl.maxPermissionCheckEnabled, default falsesystem.acl.maxPermissionChecks, default 1000system.acl.maxPermissionCheckTimeMillis, default 10000Worth knowing because "the query returns fewer results than I expected" is usually either permission filtering or these caps.
These configure the indexing path. Every one of them can be supplied three ways, and this is where a lot of confusion comes from, so it is worth stating the rule once.
One property, three spellings. The indexer is a Spring Boot application, so relaxed binding applies: to turn a property name into an environment variable, uppercase it, replace
.with_, and delete any-. Soalfresco.reindex.continuous.maxWindowisALFRESCO_REINDEX_CONTINUOUS_MAXWINDOW, andalfresco.accepted-content-media-types-cache.base-urlisALFRESCO_ACCEPTEDCONTENTMEDIATYPESCACHE_BASEURL. The documentation shows the kebab-case form in the property table, the camelCase form on the command line, and the uppercase form in Compose. They are all the same property, not three different ones.
spring.datasource.url, default jdbc:postgresql://localhost:5432/alfresco. Repository database URL.spring.datasource.username, default alfresco. Use a read-only user. The indexer never writes here.spring.datasource.password, default alfresco. Override this. The shipped default is a placeholder.spring.datasource.hikari.maximumPoolSize, default 20. Connection pool ceiling.alfresco.db.minimum.schema.version, default 14002. Refuses to start against an older repository schema.spring.elasticsearch.uris, default http://localhost:9200. Comma-separated node URIs. Requests are load-balanced across them and fail over when a node is unavailable. Credentials may be embedded, for example https://user:password@host:9200. Use this property for new configurations. spring.elasticsearch.rest.uris is a legacy alias that still works, and it is what a lot of older Compose files use.spring.elasticsearch.username, empty by default. HTTP Basic user name.spring.elasticsearch.password, empty by default. HTTP Basic password.spring.elasticsearch.auth-mode, default basic. basic or aws-iam. An unsupported value fails fast at property binding.spring.elasticsearch.aws-region, empty by default. Region for SigV4 signing when auth-mode=aws-iam.spring.elasticsearch.aws-service, default es. AWS signing service name.spring.elasticsearch.path-prefix, empty by default. Path prefix for clusters behind a sub-path.spring.elasticsearch.restclient.sniffer.enabled, default false. Automatic node discovery. See the warning below.elasticsearch.indexName, default alfresco. Must match the repository's elasticsearch.indexName.The sniffing trap. Sniffing is off by default for a good reason. When enabled, the client discards the URIs you configured and replaces them with the addresses the cluster advertises. In a container network those advertised addresses are frequently unreachable, so the connection succeeds, then fails immediately after discovery. Leave it off unless you have verified the cluster publishes routable addresses.
The most important set of properties in this post. Durations accept 30s, 10m, 24h style values.
All of the names below take the prefix alfresco.reindex.continuous.
autoStart, default true. Start scheduled indexing at application startup. Set false to deploy without indexing yet.pollingInterval, default 30s. Fixed delay between completed cycles. Lower means fresher results and more load on the database and the cluster.maxPollingInterval, default 5m. Upper bound for exponential backoff after consecutive failures.catchUpPollingInterval, default 1s. Delay between chunks while working through a backlog.overlap, default 10m. Look-back applied to every cycle so boundary changes are not missed.maxWindow, default 30m. Maximum time span a single cycle processes. Larger gaps are processed in chunks of this size.maxGapAge, default 24h. Maximum age for automatic gap recovery. Anything older is skipped with a warning. 0 removes the cap, which is what you need for a full initial index.watermarkIndexName, default alfresco-reindex-state. Index holding the cursor.watermarkRetryMaxAttempts, default 3. Retries for cursor reads and writes.watermarkRetryDelay, default 2s. Initial delay between cursor retries.stuckThreshold, default 10m. How long the scheduler may be silent (no cycle start, no chunk commit, no cycle finish) before the liveness endpoint reports DOWN. Must be greater than the worst-case single-chunk wall-clock time, otherwise a legitimate backoff triggers a spurious restart. The application validates that it exceeds maxPollingInterval and refuses to start if it does not.autoCreateStateIndex, default true. Create the state index at startup if missing. Set false to refuse to start when the state index has vanished, which protects you from an accidental full rescan.bootstrapFromAlfrescoIndex, default true. When there is no cursor, seed it from the latest indexed change in the main index, minus the overlap.requireAlfrescoIndex, default false. When the cursor records real progress but the main index is missing, refuse to start rather than silently re-indexing nothing.Those last three are the guard rails, and they deserve more attention than they usually get. The failure mode they prevent is quiet and expensive: the state index disappears, the indexer helpfully recreates it, starts from now - overlap, and you have a silently stale index that looks healthy. Setting autoCreateStateIndex=false in production turns that into a loud startup failure with an actionable message instead.
alfresco.reindex.metadataIndexingEnabled, default true. Index node metadata.alfresco.reindex.contentIndexingEnabled, default true. Index extracted full text. Turning this off removes all Transform Core dependency.alfresco.reindex.pathIndexingEnabled, default true. Index paths, which is what makes PATH and site-scoped queries work.alfresco.reindex.pathCacheSize, default 10000. Path resolution cache size.alfresco.reindex.jobName, default reindexByDate. The Spring Batch job name. You will see it in the logs.alfresco.reindex.batchSize, default 1000. Items per batch write.alfresco.reindex.pagesize, default 1000. Rows read per database page. Note the lowercase s, unlike every sibling property.alfresco.reindex.concurrentProcessors, default 10. Worker threads for the multi-threaded step.alfresco.reindex.multithreadedStepEnabled, default true. Spread chunks across worker threads.alfresco.reindex.skipLimit, default 100. How many items a cycle may skip before the step fails. Skipped items land in the dead-letter index. The shared reindexing module defaults this to 0; the batch indexer opts in to 100 so that a single poison node cannot stall continuous indexing.alfresco.reindex.retryingEnabled, default true. Retry transient failures.alfresco.reindex.retryingMaxCount, default 3. Retry attempts.alfresco.reindex.retryingInitialDelay, default 1000 ms. First backoff delay.alfresco.reindex.retryingDelayIntervalMultiplier, default 2. Backoff multiplier.alfresco.reindex.retryingMaxDelay, default 30000 ms. Backoff ceiling.alfresco.reindex.writerRetryCount, default 3. Retries for an individual bulk write.alfresco.reindex.writerRetryDelay, default 1000 ms. Delay between bulk-write retries.alfresco.acs.url, default http://localhost:8080. Base URL of Content Services.alfresco.content.transform.urlPath, default /alfresco/service/api/solr/textContent. The text-extraction endpoint.alfresco.content.transform.sharedSecret, empty by default. Must match solr.sharedSecret. Sent as X-Alfresco-Search-Secret.alfresco.content.transform.timeout, default 20s. Per-request timeout.alfresco.content.transform.retryMaxAttempts, default 2. Retries per content fetch.alfresco.content.transform.retryDelay, default 1s. Delay between those retries.alfresco.content.transform.writeConcurrency, default 16. Parallel content fetches per chunk, on a dedicated fork-join pool. Sized to roughly match Transform Core's worker count.alfresco.content.transform.maxResponseSizeMb, default 10. Caps the extracted-text response buffer. See the warning below.alfresco.accepted-content-media-types-cache.base-url, default http://localhost:8090/transform/config. Transform Core config endpoint, used to learn which media types can be transformed.alfresco.accepted-content-media-types-cache.enabled, default true. Cache that response.alfresco.cache.timeout.seconds, default 120. Metadata cache lifetime.The 10 MB text cap is worth knowing about.
maxResponseSizeMblimits the extracted text, not the original file. A 400 MB video is fine because its text extraction is tiny; a 30 MB plain-text log file is not. Documents whose extracted text exceeds the cap keep failing on every overlap and stay silently absent from full-text search, while their metadata and path index normally, so they still appear in metadata queries. Those give-ups are recorded in the dead-letter index perdbId, which is how you find them. Worst-case in-flight memory iswriteConcurrencymultiplied bymaxResponseSizeMb, so raising both at once is how you run the indexer out of heap.
alfresco.reindex.dead-letter.enabled, default true. Record give-up events. Writes are best effort and rate-limited to one per minute per failure type, so a broken dead-letter index never blocks indexing.alfresco.reindex.dead-letter.indexName, default alfresco-reindex-dead-letter. Index name.alfresco.reindex.failOnMissingTag, default true. Tag handling.alfresco.reindex.addEmptyTagAttribute, default true. Tag handling.alfresco.reindex.removeTaggableAttribute, default true. Tag handling.alfresco.metrics.reindex.max-retained-jobs, default 50. How many recent cycles keep their per-cycle metrics. A new cycle starts every polling interval, so this bounds meter growth. 0 or negative retains all cycles, unbounded.server.port, default 8080. Actuator port.management.endpoints.web.exposure.include, default health,info,prometheus,metrics. Exposed Actuator endpoints.management.endpoint.health.show-details, default never. Health detail visibility.Start from an honest baseline: the reference deployment is not hardened. The Compose files that ship as examples, and the ones the installer generates, run OpenSearch with DISABLE_SECURITY_PLUGIN=true. The only authentication actually configured out of the box is the shared-secret header protecting the repository's text-extraction endpoint. That is fine for a laptop and not fine for anything else.
Here is what is genuinely available, separated from what is not, because this is an area where it is easy to assume more than the product offers.
elasticsearch.secureComms=nonehttp:// in spring.elasticsearch.uriselasticsearch.secureComms=https, server certificate verified against the repository truststore (encryption.ssl.truststore.*)https:// in spring.elasticsearch.uris, trust configured through the JVM (-Djavax.net.ssl.trustStore)elasticsearch.user and elasticsearch.passwordspring.elasticsearch.username and password, or credentials embedded in the URIelasticsearch.auth.mode=aws-iam plus elasticsearch.aws.region and elasticsearch.aws.servicespring.elasticsearch.auth-mode=aws-iam plus aws-region and aws-serviceWhat is not there. The authentication mode enum has exactly two values,
basicandaws-iam, on both sides. There is no JWT or bearer-token configuration anywhere in the batch indexer or in the repository subsystem, so if your OpenSearch cluster authenticates via an identity provider token you will need a proxy in front of it. mTLS is not a configurable auth mode either; the client inherits mTLS support from the shared indexing library, and in practice you configure it with JVM keystore and truststore flags rather than with application properties. LDAP and Active Directory need no special client configuration at all, because they end up as a user name and a password over Basic auth, but they do require user synchronisation on the Alfresco side so that permission filtering resolves the right authorities.
application.properties on disk.The two products share a source base, and that is the direct reason the Community source is not published: it reuses Enterprise code. But sharing a subsystem does not mean sharing an indexing mechanism, and the difference is fundamental.
elasticsearch subsystem, so this part is identical: same query languages, same field support, same limitations.The query path being identical is the good news: everything you know about querying Alfresco Search Enterprise transfers directly. The indexing path being different is where the operational differences live, and one of them deserves its own callout.
Only one indexer may write to a cluster at a time. The application uses optimistic concurrency control on the cursor document as a lock. If a second instance detects a conflicting write, it terminates rather than continuing with interleaved writes. So you cannot scale indexing horizontally, and you cannot run active-active. For Kubernetes this means a single replica, and it means a rolling update briefly has no indexer running, which is harmless because the cursor guarantees the new pod resumes exactly where the old one stopped.
There is also one asymmetry in the query surface that is genuinely surprising given the shared code: the fields that describe the indexing mechanism itself, DBID, TXID, TXCOMMITTIME and friends, are not queryable, even though the Community mechanism is the one that works in terms of transactions and database identifiers. More on that in the query section below.
The section above says the mechanism differs. This one is about what that costs, because polling instead of consuming events is the single fact that governs how Alfresco Search Community behaves under load. It cuts both ways: worse latency on a quiet repository, better throughput on a busy one.
Community's indexing lag is not simply the polling interval. Three things stack up:
pollingInterval after the previous one finished, not 30 seconds after the previous one started. A cycle that does 20 seconds of work therefore repeats every 50 seconds.Add them up and the floor for a single change is about one polling interval plus one cycle's wall time plus a refresh. The worst case, a commit landing just after a window closed, is roughly twice that. Enterprise has no equivalent stack: a node change produces a message, the message is routed once and consumed once, and the write follows immediately.
The gap in what you can observe is wider than the gap in the mechanism. Enterprise publishes indexing lag directly, as alfresco.live.indexing.lag and alfresco.live.indexing.reception.lag, both in seconds, plus timers that split content extraction into transform time and ingest time. Community publishes none of these, because there is no source event timestamp to measure against. What it does publish is the cursor, and the distance between the cursor and now is your indexing lag.
If you alert on one thing, alert on the cursor gap. It is the only lag signal Community has. Every other indexing metric tells you about work done, not about how far behind you are.
This is the part worth internalising, because it explains why the two products degrade in opposite directions.
Enterprise works in documents. One node event becomes one indexing request carrying exactly one document. There is no aggregation anywhere on the live path and no batch-size setting to tune, because nothing is ever batched: a bulk request on that path holds a single operation. That is efficient when a handful of nodes change, and the cost is strictly proportional to how much actually changed.
Community works in transactions and time windows. Each cycle reads a window of committed transactions from the database, pages the nodes out at pagesize, and writes them to the search cluster as one bulk request per chunk of batchSize. At a thousand documents per request the per-document overhead is a fraction of what Enterprise pays, which is why bulk loading is the case Community handles well.
The catch is the floor. Every cycle re-reads the last overlap of history, ten minutes by default, so that a transaction committed near a window boundary cannot slip through. That re-read happens whether or not anything changed. Community's steady-state cost is therefore set by overlap and the shape of the repository's recent history rather than by the change rate, and an idle repository is not free.
Neither product scales its front door, which is more symmetry than the architecture diagrams suggest.
For Community the constraint is the single writer described above. Worth adding to it: the losing instance does not fail fast. It starts, runs a complete cycle including writes to the search cluster, and only discovers the conflict when it tries to advance the cursor. Two replicas therefore means duplicated indexing work followed by a crash loop, not a clean rejection at startup.
Enterprise is more nuanced than "it scales". Its pipeline has four stages and they do not scale alike:
Inside Community's single process there is real parallelism, and it is the only parallelism available: concurrentProcessors threads working on chunks, and writeConcurrency parallel content fetches within each chunk.
Enterprise splits initial load and steady state into two different applications. Live indexing keeps the index current from events. A separate reindexing application does the historical pass, and that one reads the repository database directly over JDBC, which makes it the closer relative of the Community indexer. It runs as a one-shot job in one of two modes, over a node-identifier range or over a date range, and it has two capabilities Community does not:
Community's batch indexer is built from that reindexing application, with the messaging removed and a continuous scheduler added. That is the whole explanation for the shape of its performance envelope: the historical pass and the ongoing sync are the same code running on the same schedule in one process, so there is no partitioning to enable and no separate tuning profile for the initial load. What you tune for the first index is what you then live with, unless you change the settings again afterwards.
For a small repository that matters less than it sounds. The migration section below reports the initial catch-up for a demo repository of roughly 870 nodes finishing in about a minute. Do not scale that figure linearly, for the reason in the next subsection.
Metadata indexing is fast on both products. Extracting text from documents is not, and it dominates everything else as soon as real content is involved.
Community does it synchronously. For each content-bearing node it calls the repository's text-content endpoint over HTTP, with a 20 second timeout and two retries, and the chunk thread blocks until every fetch in that chunk has returned. Because chunk threads and per-chunk fetches multiply, the shipped defaults can put well over a hundred concurrent extraction requests against the repository and the Transform Service. That is the setting most likely to hurt you, and it is why an indexer that looks idle is often just waiting on transforms.
Enterprise moves the same work off the critical path. The event goes to a queue, a transform request goes to the Transform Service, the reply returns on another queue, the extracted text is fetched from the shared file store, and only then is the document updated: six hops, all asynchronous, with timers on the stages. More moving parts, but no thread blocked waiting.
The shared constraint is the Transform Service itself, and Enterprise's documentation is explicit about it: keep transform request consumers below ten, or transformations get missed. Both products drive that same service, so treat it as the ceiling on either.
Shipped defaults for the 5.7.1 indexer, in the order worth touching:
alfresco.reindex.continuous.pollingInterval, default 30s. Lower it for fresher results, at the cost of re-reading the overlap window more often.alfresco.reindex.continuous.maxWindow, default 30m. The cap on how much history one cycle may cover. Raise it for an initial load so catch-up proceeds in larger chunks.alfresco.reindex.continuous.overlap, default 10m. Your safety margin against boundary misses, and also your idle cost. Do not set it to zero.alfresco.reindex.batchSize, default 1000. Both the chunk size and the bulk request size. This is the throughput knob.alfresco.reindex.pagesize, default 1000. How many rows come back from the database per page.alfresco.reindex.concurrentProcessors, default 10. Chunk threads. Raising it raises database and cluster pressure together.alfresco.content.transform.writeConcurrency, default 16. Parallel content fetches per chunk. It multiplies against concurrentProcessors, so treat the product of the two as your real concurrency against the repository.spring.datasource.hikari.maximumPoolSize, default 20. Raise it alongside concurrentProcessors, or the threads will queue waiting for connections.Two of these are validated at startup and will stop the application rather than misbehave quietly: maxWindow must be greater than pollingInterval, and catchUpPollingInterval must be less than it.
Check the metric cardinality before you build dashboards. Every cycle is a new job with a new
jobId, and every reindex metric carries that tag. At the default interval that is roughly 2,880 new tag values a day. The application retains only the fifty most recent jobs and evicts the rest, so ajobId-keyed dashboard shows a sliding window, and a long-term metrics store accumulates series that never repeat. Aggregate acrossjobIdrather than grouping by it.
When the indexer starts, or resumes after an outage, it compares the cursor against the present and picks one of three behaviours
Gap Recovery
Treat
maxGapAgeas a data-loss boundary. If the indexer has been down longer thanmaxGapAge(24 hours by default), it does not catch up. It fast-forwards tonow - maxGapAge, logs a warning, and everything in the skipped interval is simply never indexed. Nothing errors, nothing retries, and the index looks healthy. Set it wide enough to cover a realistic worst-case outage, including a long weekend, and alert on the warning.
Spring Boot Actuator on port 8080:
/actuator/health - overall health./actuator/health/liveness - liveness probe. Includes continuousReindexingMonitor, so a scheduler stuck for longer than stuckThreshold reports DOWN and Kubernetes restarts the pod./actuator/info - build information./actuator/metrics - application and JVM metrics./actuator/prometheus - the same metrics in Prometheus text format, for scraping.The single most useful thing to monitor is not any of those, though. It is whether the cursor is keeping up:
curl -s http://localhost:9200/alfresco-reindex-state/_doc/reindexByDate-watermark \
| python3 -m json.tool
That document is the whole state of the system. Its fields are lastSuccessfulFromTimeEpochMs, lastSuccessfulToTimeEpochMs, lastRunStatus, lastRunReadCount, lastRunWriteCount, lastRunSkipCount and updatedAt. If lastSuccessfulToTimeEpochMs stops closing on the present, you have a problem, and the size of the gap is your indexing lag in milliseconds. Alert on that.
Every give-up is recorded in alfresco-reindex-dead-letter, keyed by node or by failed window, with the failure stage (reader, processor, or one of the writers), the failure type, a failure count, and first and last failure times. Note that the index is created with no mapping at all, so it looks empty and schemaless until the first failure arrives and OpenSearch infers a schema.
Recovery is a manual task. There is no retry endpoint and no automatic replay. You fix the upstream cause and let a later cycle re-index the affected nodes. In practice that means the dead-letter index is an audit trail that outlives log rotation and container restarts, which is exactly what it was designed to be.
INFO by default. Watch for startup, cursor bootstrap, and cycle-completion messages. Raise org.alfresco.elasticsearch.batchindexing to DEBUG only for incident triage: debug output includes node identifiers and window bounds, but never content.A useful marker while a backlog is being processed: catch-up cycles log [chunked gap recovery - more chunks pending], so docker compose logs -f batch-indexer | grep "reindexByDate cycle" gives you a live view of progress through history.
Two facts to set expectations before anything else. There is no data migration between Solr and OpenSearch. The schemas have nothing in common and no conversion tool exists. The path is always a full reindex. And the default configuration will not do it for you: as covered above, a fresh indexer starts at now - overlap and never looks at history. Getting ten years of repository into a new index is a deliberate, manual procedure.
Migration Phases
The ordering of steps 2 and 3 is not arbitrary, and this is the part that is easy to get wrong. The index and its mapping are created by Content Services, not by the indexer: elasticsearch.createIndexIfNotExists and the ContentModelSynchronizer both live in the repository's elasticsearch subsystem. So the subsystem has to be active before the indexer has anything to write into.
The consequence is that from step 2 onwards, searches are served from an index that is still catching up, and results will be incomplete until step 4 finishes. You have two ways to handle that: run the whole thing in a maintenance window, or keep Solr alive as a rollback target.
Remember that search.solrTrackingSupport.enabled defaults to false under the elasticsearch subsystem. That default quietly kills the tracking endpoints Solr polls, so if you leave it alone, your Solr index starts going stale the moment you switch subsystems, and by the time you discover a problem the rollback target is useless. Force it on for the duration of the migration:
-Dindex.subsystem.name=elasticsearch
-Dsearch.solrTrackingSupport.enabled=true
Now both engines are live: OpenSearch answers queries while it catches up, and Solr keeps tracking so it stays current. If you need to roll back, point index.subsystem.name at solr6 and restart. One caveat: while you are on solr6, the content-model synchronizer is not running, so content-model changes made during that period will not reach the OpenSearch mapping.
This is the step the official documentation describes in prose but does not give you an API call for, so here it is. Two properties and one HTTP request.
First, remove the gap-age cap, otherwise the historical cursor you are about to write is older than maxGapAge and gets discarded on the very first cycle:
ALFRESCO_REINDEX_CONTINUOUS_MAXGAPAGE: 0
Then write a starting cursor. The simple version, using a fixed date safely before your repository existed:
# 1262304000000 = 2010-01-01T00:00:00Z
curl -X PUT "$OS/alfresco-reindex-state/_doc/reindexByDate-watermark" \
-H 'Content-Type: application/json' \
-d '{"schemaVersion":1,"lastSuccessfulToTimeEpochMs":1262304000000}'
Do not use a fixed date on a real repository. Catch-up walks the calendar, not the documents: one
maxWindowchunk per cycle regardless of whether that window contains any changes. Seed at 2010 with the default 30 minute window and you have committed to roughly 280,000 cycles of empty windows before the indexer reaches 2026. Seed from the repository's own first transaction instead, and the catch-up lasts in proportion to the history that actually exists.
For a repository with years of history, raise the window so you are not spending a cycle per half hour of calendar time. MAX_WINDOW=7d is a reasonable starting point, and the documentation also suggests sizing batchSize and concurrentProcessors up for the initial load, since it reads and writes far more than steady state.
Attaching to an already-populated index? Then do not seed anything. Leave
bootstrapFromAlfrescoIndexat its default oftrueand the cursor resumes near the existing index progress instead of re-indexing from the start.
Four checks, in order:
lastSuccessfulToTimeEpochMs stops trailing and starts tracking within one polling interval of now.Some commands for those:
# cursor position
curl -s "$OS/alfresco-reindex-state/_doc/reindexByDate-watermark" | python3 -m json.tool
# document count in the main index
curl -s "$OS/alfresco/_count"
# anything in the dead-letter index?
curl -s "$OS/alfresco-reindex-dead-letter/_search?size=20&expand_wildcards=all"
# a real search, through Content Services
curl -s -u admin:admin -X POST \
"http://localhost:8080/alfresco/api/-default-/public/search/versions/1/search" \
-H 'Content-Type: application/json' \
-d '{"query":{"language":"afts","query":"budget"}}'
Then return the scheduling properties to steady state, which mainly means putting the gap cap back:
ALFRESCO_REINDEX_CONTINUOUS_MAXGAPAGE: 24h
ALFRESCO_REINDEX_CONTINUOUS_MAXWINDOW: 30m
And only then stop Solr and set search.solrTrackingSupport.enabled back to its default.
For scale: a demo repository catches up in minutes. Real numbers depend almost entirely on how much text there is to extract, because content transformation, not indexing, is the bottleneck.
Mostly yes, and the exceptions matter more than the count suggests. AFTS, Lucene and CMIS are all supported on the elasticsearch subsystem. What changes is the set of pseudo-fields available, and, critically, how the system behaves when you use one that is not.
TYPE, EXACTTYPE, ASPECT, EXACTASPECT, CLASS, PATH, ANCESTOR, PARENT, PRIMARYPARENT, TEXT, ALL, ID, ISNODE, ISNOTNULL, EXISTS, ISNULL, ISUNSET, OWNER, READER, DENIED, AUTHORITY, TAG, SITE.PATHWITHREPEATS, PNAME, NPATH, QNAME, PRIMARYASSOCQNAME, PRIMARYASSOCTYPEQNAME, OWNERSET, READERSET, DENYSET, AUTHORITYSET (AUTHSET), DBID, TX, TXID, INTXID, TXCOMMITTIME, ACLID, INACLTXID, ACLTXID, ACLTXCOMMITTIME, FTSSTATUS, ISROOT, ISCONTAINER, TENANT, FINGERPRINT, id, CASCADETX.A few of the supported ones come with caveats: CLASS works for term queries only, PATH silently ignores invalid XPath, and ANCESTOR and PARENT do not support category paths. TEXT expands to cm:name, cm:title, cm:description and cm:content. Three more fields, ASSOCTYPEQNAME, ISCATEGORY and LINKASPECT, are not special-cased at all and get treated as ordinary property names.
The loss of FINGERPRINT is the one most likely to break a feature rather than a query: that is document-similarity search, and there is no equivalent.
This is the single most important thing in this post. An unsupported condition does not raise an error. The parser accepts it, because the AFTS and CMIS grammars are shared between both subsystems, and then the query builder returns
nullfor that condition, logs a WARN, and the query proceeds without it. So a query written to narrow a result set can silently widen it instead.banana AND TXCOMMITTIME:taxireturns every document matchingbanana, with HTTP 200 and no indication that half of your query was discarded.
Think about what that means for anything that uses search as a filter: a rule, a scheduled job, a report, a permission-adjacent listing. Under solr6 a broken query returned nothing and somebody noticed. Under elasticsearch it returns too much and nobody does.
The only warning you get is in the repository log, so grep for it:
docker compose logs -f alfresco \
| grep -E "Ignoring query condition|Ignorning sort on field"
The second pattern is misspelled in the product source, so match both spellings rather than the correct one.
Because silent drops return HTTP 200, you cannot detect them from a single response. You have to test differentially: run a baseline query and a supposedly narrower one, and if the counts are identical, the restriction was dropped. There is a companion script for this that runs 25 documented cases as 28 checks against a live instance, grouped into loud failures, silent drops, missing response sections, and constrained behaviour. Two traps it has to work around are worth knowing about even if you write your own: every probe attaches a facet, because otherwise Transactional Metadata Query answers from the database and never exercises the index at all; and every differential check asserts a non-zero baseline first, because zero-versus-zero proves nothing.
budg* or budge?. Works, but only analyzed on the fields listed in elasticsearch.query.analyzeWildcard.fields. Rejected by design on a long list of fields including PATH, ANCESTOR, PARENT, QNAME, DBID and CLASS.budg* as a prefix term. Same reject list as wildcard. Works on TYPE, EXACTTYPE, ASPECT, EXACTASPECT, SITE, TEXT, ALL and ID.budget~0.8. Rejects more than prefix does: the prefix list plus SITE, TYPE, EXACTTYPE, ASPECT, EXACTASPECT and ISNODE.CLASS, for example CLASS:cm\:content~0.8. Throws. The one construct that fails loudly instead of being dropped.cm:created:[2024 TO 2025]. Works on TEXT, ALL, resolvable properties and datatypes. Everything else is silently ignored.=running. Disabled by default, as it is on Solr. Requires enabling datatypes or properties in exactTermSearch.properties, and because that changes the index mapping, it requires a reindex./bud.*/. Never works. Always returns "Regexp queries are not supported"."quarterly budget"~2, budget^4 or cm:modified:[NOW/DAY-7DAYS TO NOW/DAY]. Supported.Note the asymmetry there: TYPE accepts a prefix query but silently drops a fuzzy one. Field support and construct support are two separate lists, and a field being in one does not put it in the other.
SELECT ... FROM ... WHERE - supported.CONTAINS() - supported, but the AFTS limits above apply inside it.LIKE with % and _ - supported. % is translated to * unless escaped.IN_FOLDER() and IN_TREE() - supported.ORDER BY on a property - supported.cmis:objectId with a version label - supported, including private working copies and unversioned nodes.ORDER BY on a function, for example SCORE() - not supported. Throws. Fails loudly.JOIN - parses, but join support is advertised as NONE on both subsystems, so it is not executable on either. Not a regression.Three language bindings are wired to UnsupportedQueryLanguage in the Community context, so they fail rather than degrade:
xpath. This is the one to audit for. Code calling searchService.query with language xpath, and selectNodes-style usage that resolves through the search subsystem, needs review before migrating. XPath internals survive only to serve PATH: conversion, and even there only a basic subset (root, absolute, relative, child, descendant steps).index-sql, the Solr SQL endpoint. Also, there is no stats bean and no suggester bean registered on this subsystem at all, so those features are simply absent.index-alfresco and the legacy solr-* aliases.DBID, TXID and PNAME are the common ones in custom code.language values of xpath, index-sql and index-alfresco.FINGERPRINT and for ORDER BY SCORE().Ignoring query condition. Anything that logs is a query whose meaning changed even if the test still passes.Alfresco Search Community is distributed as a binary only: a Docker image and a JAR, with the source code private. As covered earlier, the reason is that the module reuses Alfresco Search Enterprise code.
The final license text has not been approved or published yet. What follows is the announced intent, and the published text is what will actually govern your use. Check it when it appears.
As announced, the terms will permit:
And will not permit:
The stated longer-term intention is to publish the source code and license it under LGPLv3, which is the license the rest of Alfresco Community uses. No date has been committed to; 27.1 has been mentioned as a possibility for the source release.
Worth addressing because the question comes up immediately once OpenSearch is in the picture: the Hyland AI Ready Index (AIR Index) also uses OpenSearch natively, including vector indexes for embeddings. So can they share a cluster?
Infrastructure yes, data no. The AIR Index indexes are entirely independent of Alfresco's. You can point both at the same OpenSearch instance to save on infrastructure, but there is no interoperability between the data: Alfresco cannot query the vector indexes and the AIR Index does not read the alfresco index. Treat it as co-tenancy, not integration.
If you do co-locate them, three things need to line up:
3.5.0, which is newer than the 2.11.1 that Alfresco Search Community lists as supported. In practice ACS 26.2 and the batch indexer work unchanged against 3.5.0, which reports a minimum wire compatibility version of 2.19.0. That is a tested observation, not a supported configuration, so verify it for your own deployment.plugins.security.disabled: "true" or the equivalent DISABLE_SECURITY_PLUGIN.knn.derived_source bug in OpenSearch 3.5, applied to the embeddings indexes:curl -X PUT "http://localhost:9200/_index_template/nuxeo-embeddings-noderivedsource" \
-H 'Content-Type: application/json' \
--data-binary '{
"index_patterns":["nuxeo_embeddings","nuxeo_embeddings_*"],
"priority":500,
"template":{"settings":{"index":{"knn":true,"knn.derived_source.enabled":false}}}
}'
The broader point is that 26.2 moves Alfresco onto the same search substrate that the AI-facing parts of the Hyland platform already use. That does not create integration on its own, but it removes the reason there could not be any.
If you read one section, read the query compatibility one. But the short version of everything above:
elasticsearch subsystem Enterprise has used since 7.1.maxGapAge=0.maxGapAge: exceed it and history is skipped silently.You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.